/**
 * Git clone lifecycle management
 *
 * Creates, removes, and tracks git clones for task isolation.
 * Uses `git clone --reference --dissociate` so each clone has a fully
 * independent .git directory, then removes the origin remote to prevent
 * Claude Code SDK from traversing back to the main repository.
 */

import * as fs from 'node:fs';
import * as path from 'node:path';
import { execFileSync } from 'node:child_process';
import { createLogger, slugify } from '../../shared/utils/index.js';
import { loadGlobalConfig } from '../config/global/globalConfig.js';
import type { WorktreeOptions, WorktreeResult } from './types.js';

export type { WorktreeOptions, WorktreeResult };

const log = createLogger('clone');

const CLONE_META_DIR = 'clone-meta';

/**
 * Manages git clone lifecycle for task isolation.
 *
 * Handles creation, removal, and metadata tracking of clones
 * used for parallel task execution.
 */
export class CloneManager {
  private static generateTimestamp(): string {
    return new Date().toISOString().replace(/[-:.]/g, '').slice(0, 13);
  }

  /**
   * Resolve the base directory for clones from global config.
   * Returns the configured worktree_dir (resolved to absolute), or ../
   */
  private static resolveCloneBaseDir(projectDir: string): string {
    const globalConfig = loadGlobalConfig();
    if (globalConfig.worktreeDir) {
      return path.isAbsolute(globalConfig.worktreeDir)
        ? globalConfig.worktreeDir
        : path.resolve(projectDir, globalConfig.worktreeDir);
    }
    return path.join(projectDir, '..');
  }

  /** Resolve the clone path based on options and global config */
  private static resolveClonePath(projectDir: string, options: WorktreeOptions): string {
    const timestamp = CloneManager.generateTimestamp();
    const slug = slugify(options.taskSlug);

    let dirName: string;
    if (options.issueNumber !== undefined && slug) {
      dirName = `${timestamp}-${options.issueNumber}-${slug}`;
    } else if (slug) {
      dirName = `${timestamp}-${slug}`;
    } else {
      dirName = timestamp;
    }

    if (typeof options.worktree === 'string') {
      return path.isAbsolute(options.worktree)
        ? options.worktree
        : path.resolve(projectDir, options.worktree);
    }

    return path.join(CloneManager.resolveCloneBaseDir(projectDir), dirName);
  }

  /** Resolve branch name from options */
  private static resolveBranchName(options: WorktreeOptions): string {
    if (options.branch) {
      return options.branch;
    }

    const slug = slugify(options.taskSlug);

    if (options.issueNumber !== undefined && slug) {
      return `takt/#${options.issueNumber}/${slug}`;
    }

    const timestamp = CloneManager.generateTimestamp();
    return slug ? `takt/${timestamp}-${slug}` : `takt/${timestamp}`;
  }

  private static branchExists(projectDir: string, branch: string): boolean {
    try {
      execFileSync('git', ['rev-parse', '--verify', branch], {
        cwd: projectDir,
        stdio: 'pipe',
      });
      return true;
    } catch {
      return false;
    }
  }

  /**
   * Resolve the main repository path (handles git worktree case).
   * If projectDir is a worktree, returns the main repo path.
   * Otherwise, returns projectDir as-is.
   */
  private static resolveMainRepo(projectDir: string): string {
    const gitPath = path.join(projectDir, '.git');

    try {
      const stats = fs.statSync(gitPath);
      if (stats.isFile()) {
        const content = fs.readFileSync(gitPath, 'utf-8');
        const match = content.match(/^gitdir:\s*(.+)$/m);
        if (match && match[1]) {
          const worktreePath = match[1].trim();
          const gitDir = path.resolve(worktreePath, '..', '..');
          const mainRepoPath = path.dirname(gitDir);
          log.info('Detected worktree, using main repo', { worktree: projectDir, mainRepo: mainRepoPath });
          return mainRepoPath;
        }
      }
    } catch (err) {
      log.debug('Failed to resolve main repo, using projectDir as-is', { error: String(err) });
    }

    return projectDir;
  }

  /** Clone a repository and remove origin to isolate from the main repo */
  private static cloneAndIsolate(projectDir: string, clonePath: string): void {
    const referenceRepo = CloneManager.resolveMainRepo(projectDir);

    fs.mkdirSync(path.dirname(clonePath), { recursive: true });

    execFileSync('git', ['clone', '--reference', referenceRepo, '--dissociate', projectDir, clonePath], {
      cwd: projectDir,
      stdio: 'pipe',
    });

    execFileSync('git', ['remote', 'remove', 'origin'], {
      cwd: clonePath,
      stdio: 'pipe',
    });

    // Propagate local git user config from source repo to clone
    for (const key of ['user.name', 'user.email']) {
      try {
        const value = execFileSync('git', ['config', '--local', key], {
          cwd: projectDir,
          stdio: 'pipe',
        }).toString().trim();
        if (value) {
          execFileSync('git', ['config', key, value], {
            cwd: clonePath,
            stdio: 'pipe',
          });
        }
      } catch {
        // not set locally — skip
      }
    }
  }

  private static encodeBranchName(branch: string): string {
    return branch.replace(/\//g, '--');
  }

  private static getCloneMetaPath(projectDir: string, branch: string): string {
    return path.join(projectDir, '.takt', CLONE_META_DIR, `${CloneManager.encodeBranchName(branch)}.json`);
  }

  /** Create a git clone for a task */
  createSharedClone(projectDir: string, options: WorktreeOptions): WorktreeResult {
    const clonePath = CloneManager.resolveClonePath(projectDir, options);
    const branch = CloneManager.resolveBranchName(options);

    log.info('Creating shared clone', { path: clonePath, branch });

    CloneManager.cloneAndIsolate(projectDir, clonePath);

    if (CloneManager.branchExists(clonePath, branch)) {
      execFileSync('git', ['checkout', branch], { cwd: clonePath, stdio: 'pipe' });
    } else {
      execFileSync('git', ['checkout', '-b', branch], { cwd: clonePath, stdio: 'pipe' });
    }

    this.saveCloneMeta(projectDir, branch, clonePath);
    log.info('Clone created', { path: clonePath, branch });

    return { path: clonePath, branch };
  }

  /** Create a temporary clone for an existing branch */
  createTempCloneForBranch(projectDir: string, branch: string): WorktreeResult {
    const timestamp = CloneManager.generateTimestamp();
    const clonePath = path.join(CloneManager.resolveCloneBaseDir(projectDir), `tmp-${timestamp}`);

    log.info('Creating temp clone for branch', { path: clonePath, branch });

    CloneManager.cloneAndIsolate(projectDir, clonePath);
    execFileSync('git', ['checkout', branch], { cwd: clonePath, stdio: 'pipe' });

    this.saveCloneMeta(projectDir, branch, clonePath);
    log.info('Temp clone created', { path: clonePath, branch });

    return { path: clonePath, branch };
  }

  /** Remove a clone directory */
  removeClone(clonePath: string): void {
    log.info('Removing clone', { path: clonePath });
    try {
      fs.rmSync(clonePath, { recursive: true, force: true });
      log.info('Clone removed', { path: clonePath });
    } catch (err) {
      log.error('Failed to remove clone', { path: clonePath, error: String(err) });
    }
  }

  /** Save clone metadata (branch → clonePath mapping) */
  saveCloneMeta(projectDir: string, branch: string, clonePath: string): void {
    const filePath = CloneManager.getCloneMetaPath(projectDir, branch);
    fs.mkdirSync(path.dirname(filePath), { recursive: true });
    fs.writeFileSync(filePath, JSON.stringify({ branch, clonePath }));
    log.info('Clone meta saved', { branch, clonePath });
  }

  /** Remove clone metadata for a branch */
  removeCloneMeta(projectDir: string, branch: string): void {
    try {
      fs.unlinkSync(CloneManager.getCloneMetaPath(projectDir, branch));
      log.info('Clone meta removed', { branch });
    } catch {
      // File may not exist — ignore
    }
  }

  /** Clean up an orphaned clone directory associated with a branch */
  cleanupOrphanedClone(projectDir: string, branch: string): void {
    try {
      const raw = fs.readFileSync(CloneManager.getCloneMetaPath(projectDir, branch), 'utf-8');
      const meta = JSON.parse(raw) as { clonePath: string };
      if (fs.existsSync(meta.clonePath)) {
        this.removeClone(meta.clonePath);
        log.info('Orphaned clone cleaned up', { branch, clonePath: meta.clonePath });
      }
    } catch {
      // No metadata or parse error — nothing to clean up
    }
    this.removeCloneMeta(projectDir, branch);
  }
}

// ---- Module-level functions ----

const defaultManager = new CloneManager();

export function createSharedClone(projectDir: string, options: WorktreeOptions): WorktreeResult {
  return defaultManager.createSharedClone(projectDir, options);
}

export function createTempCloneForBranch(projectDir: string, branch: string): WorktreeResult {
  return defaultManager.createTempCloneForBranch(projectDir, branch);
}

export function removeClone(clonePath: string): void {
  defaultManager.removeClone(clonePath);
}

export function saveCloneMeta(projectDir: string, branch: string, clonePath: string): void {
  defaultManager.saveCloneMeta(projectDir, branch, clonePath);
}

export function removeCloneMeta(projectDir: string, branch: string): void {
  defaultManager.removeCloneMeta(projectDir, branch);
}

export function cleanupOrphanedClone(projectDir: string, branch: string): void {
  defaultManager.cleanupOrphanedClone(projectDir, branch);
}
