/**
 * SimpleGit WorktreeManager Adapter
 *
 * Implementation of WorktreeManager port interface using simple-git.
 * This is the ADAPTER for the WorktreeManager PORT.
 *
 * Manages git worktrees for isolated agent workspaces.
 * Each agent gets its own worktree to avoid file conflicts.
 */

import path from 'node:path';
import { simpleGit, type SimpleGit } from 'simple-git';
import type { EventBus } from '../events/types.js';
import type {
  WorktreeManager,
  Worktree,
  WorktreeDiff,
  MergeResult,
} from './types.js';
import { createModuleLogger } from '../logger/index.js';

const log = createModuleLogger('git');

/**
 * SimpleGit-based implementation of the WorktreeManager interface.
 *
 * Wraps simple-git to provide git worktree operations
 * that conform to the WorktreeManager port interface.
 */
export class SimpleGitWorktreeManager implements WorktreeManager {
  private git: SimpleGit;
  private repoPath: string;
  private worktreesDir: string;
  private eventBus?: EventBus;

  /**
   * Create a new SimpleGitWorktreeManager.
   *
   * @param repoPath - Absolute path to the git repository
   * @param eventBus - Optional EventBus for emitting git events
   * @param worktreesBaseDir - Optional custom base directory for worktrees (defaults to <repoPath>/.cw-worktrees)
   */
  constructor(repoPath: string, eventBus?: EventBus, worktreesBaseDir?: string) {
    this.repoPath = repoPath;
    this.git = simpleGit(repoPath);
    this.worktreesDir = worktreesBaseDir ?? path.join(repoPath, '.cw-worktrees');
    this.eventBus = eventBus;
  }

  /**
   * Create a new worktree for isolated agent work.
   *
   * Creates a new branch and worktree directory.
   * The worktree will be ready for the agent to start working.
   */
  async create(
    id: string,
    branch: string,
    baseBranch: string = 'main'
  ): Promise<Worktree> {
    const worktreePath = path.join(this.worktreesDir, id);
    log.info({ id, branch, baseBranch }, 'creating worktree');

    // Create worktree — reuse existing branch or create new one
    const branchExists = await this.branchExists(branch);
    if (branchExists) {
      // Branch exists from a previous run — reset it to baseBranch and check it out
      await this.git.raw(['branch', '-f', branch, baseBranch]);
      await this.git.raw(['worktree', 'add', worktreePath, branch]);
    } else {
      // git worktree add -b <branch> <path> <base-branch>
      await this.git.raw(['worktree', 'add', '-b', branch, worktreePath, baseBranch]);
    }

    const worktree: Worktree = {
      id,
      branch,
      path: worktreePath,
      isMainWorktree: false,
    };

    // Emit event if eventBus provided
    this.eventBus?.emit({
      type: 'worktree:created',
      timestamp: new Date(),
      payload: {
        worktreeId: id,
        branch,
        path: worktreePath,
      },
    });

    return worktree;
  }

  /**
   * Remove a worktree and optionally its branch.
   *
   * Cleans up the worktree directory and removes it from git's tracking.
   */
  async remove(id: string): Promise<void> {
    const worktree = await this.get(id);
    if (!worktree) {
      throw new Error(`Worktree not found: ${id}`);
    }

    const branch = worktree.branch;
    log.info({ id, branch }, 'removing worktree');

    // Remove worktree with force to handle any uncommitted changes
    // git worktree remove <path> --force
    await this.git.raw(['worktree', 'remove', worktree.path, '--force']);

    // Emit event if eventBus provided
    this.eventBus?.emit({
      type: 'worktree:removed',
      timestamp: new Date(),
      payload: {
        worktreeId: id,
        branch,
      },
    });
  }

  /**
   * List all worktrees in the repository.
   *
   * Returns all worktrees including the main one.
   */
  async list(): Promise<Worktree[]> {
    // git worktree list --porcelain
    const output = await this.git.raw(['worktree', 'list', '--porcelain']);

    return this.parseWorktreeList(output);
  }

  /**
   * Get a specific worktree by ID.
   *
   * Finds worktree by matching path ending with id.
   */
  async get(id: string): Promise<Worktree | null> {
    const worktrees = await this.list();
    return worktrees.find((wt) => wt.path.endsWith(id)) ?? null;
  }

  /**
   * Get the diff/changes in a worktree.
   *
   * Shows what files have changed compared to HEAD.
   */
  async diff(id: string): Promise<WorktreeDiff> {
    const worktree = await this.get(id);
    if (!worktree) {
      throw new Error(`Worktree not found: ${id}`);
    }

    // Create git instance for the worktree directory
    const worktreeGit = simpleGit(worktree.path);

    // Get name-status diff against HEAD
    // git diff --name-status HEAD
    let diffOutput: string;
    try {
      diffOutput = await worktreeGit.raw(['diff', '--name-status', 'HEAD']);
    } catch {
      // If HEAD doesn't exist or other issues, return empty diff
      diffOutput = '';
    }

    // Also get staged changes
    let stagedOutput: string;
    try {
      stagedOutput = await worktreeGit.raw([
        'diff',
        '--name-status',
        '--cached',
      ]);
    } catch {
      stagedOutput = '';
    }

    // Combine and parse outputs
    const combined = diffOutput + stagedOutput;
    const files = this.parseDiffNameStatus(combined);

    // Get summary
    const fileCount = files.length;
    const summary =
      fileCount === 0 ? 'No changes' : `${fileCount} file(s) changed`;

    return { files, summary };
  }

  /**
   * Merge worktree changes into target branch.
   *
   * Attempts to merge the worktree's branch into the target branch.
   * Returns conflict information if merge cannot be completed cleanly.
   */
  async merge(id: string, targetBranch: string): Promise<MergeResult> {
    const worktree = await this.get(id);
    if (!worktree) {
      throw new Error(`Worktree not found: ${id}`);
    }
    log.info({ id, targetBranch }, 'merging worktree');

    // Store current branch to restore later
    const currentBranch = await this.git.revparse(['--abbrev-ref', 'HEAD']);

    try {
      // Checkout target branch in main repo
      await this.git.checkout(targetBranch);

      // Attempt merge with no-edit (no interactive editor)
      await this.git.merge([worktree.branch, '--no-edit']);

      // Emit success event
      this.eventBus?.emit({
        type: 'worktree:merged',
        timestamp: new Date(),
        payload: {
          worktreeId: id,
          sourceBranch: worktree.branch,
          targetBranch,
        },
      });

      return {
        success: true,
        message: 'Merged successfully',
      };
    } catch (error) {
      // Check if it's a merge conflict
      const status = await this.git.status();

      if (status.conflicted.length > 0) {
        const conflicts = status.conflicted;
        log.warn({ id, targetBranch, conflictCount: conflicts.length }, 'merge conflicts detected');

        // Emit conflict event
        this.eventBus?.emit({
          type: 'worktree:conflict',
          timestamp: new Date(),
          payload: {
            worktreeId: id,
            sourceBranch: worktree.branch,
            targetBranch,
            conflictingFiles: conflicts,
          },
        });

        // Abort merge to clean up
        await this.git.merge(['--abort']);

        return {
          success: false,
          conflicts,
          message: 'Merge conflicts detected',
        };
      }

      // Some other error occurred, rethrow
      throw error;
    } finally {
      // Restore original branch if different from target
      try {
        const nowBranch = await this.git.revparse(['--abbrev-ref', 'HEAD']);
        if (nowBranch.trim() !== currentBranch.trim()) {
          await this.git.checkout(currentBranch.trim());
        }
      } catch {
        // Ignore errors restoring branch
      }
    }
  }

  /**
   * Parse the porcelain output of git worktree list.
   */
  private parseWorktreeList(output: string): Worktree[] {
    const worktrees: Worktree[] = [];
    const lines = output.trim().split('\n');

    let currentWorktree: Partial<Worktree> = {};
    let isFirst = true;

    for (const line of lines) {
      if (line.startsWith('worktree ')) {
        // Start of a new worktree entry
        if (currentWorktree.path) {
          // Derive ID from path
          const id = isFirst ? 'main' : path.basename(currentWorktree.path);
          worktrees.push({
            id,
            branch: currentWorktree.branch || '',
            path: currentWorktree.path,
            isMainWorktree: isFirst,
          });
          isFirst = false;
        }
        currentWorktree = { path: line.substring('worktree '.length) };
      } else if (line.startsWith('branch ')) {
        // Branch reference (e.g., "branch refs/heads/main")
        const branchRef = line.substring('branch '.length);
        currentWorktree.branch = branchRef.replace('refs/heads/', '');
      } else if (line.startsWith('HEAD ')) {
        // Detached HEAD, skip
      } else if (line === 'bare') {
        // Bare worktree, skip
      } else if (line === '') {
        // Empty line between worktrees
      }
    }

    // Don't forget the last worktree
    if (currentWorktree.path) {
      const id = isFirst ? 'main' : path.basename(currentWorktree.path);
      worktrees.push({
        id,
        branch: currentWorktree.branch || '',
        path: currentWorktree.path,
        isMainWorktree: isFirst,
      });
    }

    return worktrees;
  }

  /**
   * Check if a local branch exists in the repository.
   */
  private async branchExists(branch: string): Promise<boolean> {
    try {
      await this.git.raw(['rev-parse', '--verify', `refs/heads/${branch}`]);
      return true;
    } catch {
      return false;
    }
  }

  /**
   * Parse the output of git diff --name-status.
   */
  private parseDiffNameStatus(
    output: string
  ): Array<{ path: string; status: 'added' | 'modified' | 'deleted' }> {
    if (!output.trim()) {
      return [];
    }

    const lines = output.trim().split('\n');
    const files: Array<{
      path: string;
      status: 'added' | 'modified' | 'deleted';
    }> = [];
    const seen = new Set<string>();

    for (const line of lines) {
      if (!line.trim()) continue;

      // Format: "M\tpath/to/file" or "A\tpath/to/file" or "D\tpath/to/file"
      const match = line.match(/^([AMD])\t(.+)$/);
      if (match) {
        const [, statusLetter, filePath] = match;

        // Skip duplicates (can happen when combining diff + cached)
        if (seen.has(filePath)) continue;
        seen.add(filePath);

        let status: 'added' | 'modified' | 'deleted';
        switch (statusLetter) {
          case 'A':
            status = 'added';
            break;
          case 'M':
            status = 'modified';
            break;
          case 'D':
            status = 'deleted';
            break;
          default:
            continue;
        }

        files.push({ path: filePath, status });
      }
    }

    return files;
  }
}
