/**
 * GitFlow Worktree — Create, remove, repair, list, validate.
 * Self-contained: imports only from local lib + node built-ins.
 */

import { existsSync, readFileSync, writeFileSync, readdirSync, statSync, mkdirSync } from 'fs';
import { join, dirname, relative, resolve, basename } from 'path';
import type { WorktreeInfo, GitFlowConfig } from './types.js';
import { execGit, execGitOrFail, isInsideWorkTree, getAheadBehind, fetch as gitFetch, fastForwardOnly, remoteRefExists } from './git.js';
import { toForwardSlashes } from './paths.js';

export async function listWorktrees(cwd?: string): Promise<WorktreeInfo[]> {
  const result = await execGit(['worktree', 'list', '--porcelain'], cwd);
  if (result.exitCode !== 0) return [];

  const worktrees: WorktreeInfo[] = [];
  let current: Partial<WorktreeInfo> = {};

  for (const line of result.stdout.split('\n')) {
    if (line.startsWith('worktree ')) {
      if (current.path) worktrees.push(current as WorktreeInfo);
      current = { path: line.slice(9), isBare: false, isPrunable: false, branch: '', head: '' };
    } else if (line.startsWith('HEAD ')) {
      current.head = line.slice(5);
    } else if (line.startsWith('branch ')) {
      current.branch = line.slice(7).replace('refs/heads/', '');
    } else if (line === 'bare') {
      current.isBare = true;
    } else if (line === 'prunable') {
      current.isPrunable = true;
    }
  }
  if (current.path) worktrees.push(current as WorktreeInfo);

  return worktrees;
}

export async function createWorktree(
  branch: string,
  targetPath: string,
  baseBranch: string,
  cwd?: string,
): Promise<void> {
  mkdirSync(dirname(targetPath), { recursive: true });
  await execGitOrFail(
    ['worktree', 'add', '-b', branch, targetPath, `origin/${baseBranch}`],
    cwd,
  );
}

export async function removeWorktree(path: string, cwd?: string): Promise<void> {
  if (existsSync(path)) {
    await execGit(['worktree', 'remove', path, '--force'], cwd);
  }
  await execGit(['worktree', 'prune'], cwd);
}

export function getWorktreePath(branch: string, config: GitFlowConfig): string {
  const { structure } = config.worktrees;

  // When worktrees.structure is unset (uninitialized template config) the join
  // below would yield a cwd-RELATIVE path like "5.2.0", creating a parasite
  // worktree UNDER the current worktree (e.g. 02-Develop/5.2.0) instead of
  // <root>/releases/5.2.0. Derive the container dirs from the bare repo root
  // (<root>/.bare, located by walking up from cwd) so the path is ABSOLUTE.
  // A configured structure value always wins (back-compat).
  const bare = findBareDir();
  const root = bare ? dirname(bare) : '';
  const dir = (configured: string, fallback: string): string =>
    configured || (root ? join(root, fallback) : '');

  if (branch.startsWith('feature/')) return join(dir(structure.features, 'features'), branch.replace('feature/', ''));
  if (branch.startsWith('release/')) return join(dir(structure.releases, 'releases'), branch.replace('release/', ''));
  if (branch.startsWith('hotfix/')) return join(dir(structure.hotfixes, 'hotfixes'), branch.replace('hotfix/', ''));
  return '';
}

export function validateWorktreePath(worktreePath: string): { valid: boolean; error?: string } {
  const name = basename(worktreePath);
  if (/^\d{2}-/.test(name) && name !== '01-Main' && name !== '02-Develop') {
    return {
      valid: false,
      error: `Numbered directory "${name}" forbidden. Use config paths: features/{name}, releases/v{version}, hotfixes/{name}`,
    };
  }
  return { valid: true };
}

export function repairWorktreePaths(bareDir: string): { repaired: number; details: string[] } {
  const worktreesDir = join(bareDir, 'worktrees');
  if (!existsSync(worktreesDir)) return { repaired: 0, details: [] };

  let repaired = 0;
  const details: string[] = [];

  const entries = readdirSync(worktreesDir);
  for (const wtName of entries) {
    const wtDir = join(worktreesDir, wtName);
    try {
      if (!statSync(wtDir).isDirectory()) continue;

      const gitdirFile = join(wtDir, 'gitdir');
      if (!existsSync(gitdirFile)) continue;

      const storedPath = readFileSync(gitdirFile, 'utf-8').trim();

      let resolvedPath: string;
      if (/^[A-Za-z]:[/\\]/.test(storedPath) || storedPath.startsWith('/')) {
        resolvedPath = storedPath.replace(/\\/g, '/');
      } else {
        resolvedPath = resolve(wtDir, storedPath);
      }

      const wtRealDir = dirname(resolvedPath);
      if (!existsSync(wtRealDir)) continue;

      const relGitdir = toForwardSlashes(relative(resolve(wtDir), join(resolve(wtRealDir), '.git')));
      if (storedPath !== relGitdir) {
        writeFileSync(gitdirFile, relGitdir + '\n');
        details.push(`${wtName}/gitdir → ${relGitdir}`);
        repaired++;
      }

      const wtGitFile = join(wtRealDir, '.git');
      if (existsSync(wtGitFile) && statSync(wtGitFile).isFile()) {
        const gitContent = readFileSync(wtGitFile, 'utf-8').trim();
        const gitPath = gitContent.replace('gitdir: ', '');
        const relBare = toForwardSlashes(relative(resolve(wtRealDir), resolve(wtDir)));

        if (gitPath !== relBare) {
          writeFileSync(wtGitFile, `gitdir: ${relBare}\n`);
          details.push(`${wtName}/.git → ${relBare}`);
          repaired++;
        }
      }
    } catch (err: unknown) {
      const code = (err as NodeJS.ErrnoException).code;
      if (code === 'EPERM' || code === 'EBUSY') {
        details.push(`${wtName} → skipped (${code}: file locked)`);
      } else {
        throw err;
      }
    }
  }

  return { repaired, details };
}

export function findBareDir(startDir?: string): string | null {
  let dir = startDir || process.cwd();
  const root = resolve('/');

  while (dir !== root) {
    // `.bare` (legacy layout) OR a bare `.git` DIRECTORY (current init layout:
    // `git clone --bare … <root>/.git`). A bare repo holds a `worktrees/` registry
    // — that distinguishes it from a normal repo's `.git`, and from a linked
    // worktree's `.git` which is a FILE (skipped by isDirectory()).
    const dotBare = join(dir, '.bare');
    if (existsSync(dotBare) && statSync(dotBare).isDirectory()) return dotBare;
    const dotGit = join(dir, '.git');
    if (existsSync(dotGit) && statSync(dotGit).isDirectory() && existsSync(join(dotGit, 'worktrees'))) return dotGit;
    const parent = dirname(dir);
    if (parent === dir) break;
    dir = parent;
  }
  return null;
}

export async function preflightGitCheck(cwd?: string): Promise<{ ok: boolean; repaired: number }> {
  const quickTest = await execGit(['rev-parse', '--git-dir'], cwd);
  if (quickTest.exitCode === 0) return { ok: true, repaired: 0 };

  const bareDir = findBareDir(cwd);
  if (!bareDir) return { ok: false, repaired: 0 };

  const { repaired } = repairWorktreePaths(bareDir);

  const verifyTest = await execGit(['rev-parse', '--git-dir'], cwd);
  return { ok: verifyTest.exitCode === 0, repaired };
}

/**
 * Make a worktree usable for work-tree operations (status/merge/checkout/ff).
 * Repairs the inherited-bare misconfig: a LINKED worktree git treats as bare
 * (core.bare=true via extensions.worktreeConfig with no per-worktree
 * config.worktree) → set core.bare=false in ITS config.worktree. Local config
 * only — touches no refs, history or remote; safe and reversible.
 */
export async function ensureWorkTreeUsable(cwd: string): Promise<{ ok: boolean; repaired: boolean; error?: string }> {
  if (await isInsideWorkTree(cwd)) return { ok: true, repaired: false };

  // Only repair a LINKED worktree (its `.git` is a FILE pointing at the bare
  // repo). A bare root (`.git`/`.bare` directory) is not meant to be a work tree.
  const dotGit = join(cwd, '.git');
  const isLinkedWorktree = existsSync(dotGit) && statSync(dotGit).isFile();
  if (!isLinkedWorktree) {
    return { ok: false, repaired: false, error: `'${cwd}' is not a git work tree.` };
  }

  const r = await execGit(['config', '--worktree', 'core.bare', 'false'], cwd);
  if (r.exitCode === 0 && (await isInsideWorkTree(cwd))) {
    return { ok: true, repaired: true };
  }
  return {
    ok: false,
    repaired: false,
    error:
      `'${cwd}' is a linked worktree git treats as bare and auto-repair failed ` +
      `(${r.stderr || 'config --worktree core.bare false'}). Check extensions.worktreeConfig / config.worktree.`,
  };
}

// ─── Align local develop with origin before a feature PR/merge ──────────────

export type AlignAction =
  | { action: 'none' }       // aligned, ahead-only, or no remote ref yet
  | { action: 'ff' }         // behind only → fast-forward to origin
  | { action: 'diverged' };  // ahead AND behind → refuse (needs a human)

/** Pure decision for the develop↔origin alignment. */
export function decideAlignment(o: { ahead: number; behind: number; remoteExists: boolean }): AlignAction {
  if (!o.remoteExists) return { action: 'none' };
  if (o.ahead > 0 && o.behind > 0) return { action: 'diverged' };
  if (o.behind > 0) return { action: 'ff' };
  return { action: 'none' };
}

export interface DevelopAlignment {
  ok: boolean;
  worktree: string;
  ahead: number;
  behind: number;
  fastForwarded: boolean;
  diverged: boolean;
  error?: string;
}

/**
 * Ensure LOCAL develop == origin/develop BEFORE integrating a feature (the rule:
 * never PR/merge a feature onto a stale local develop). Fetches origin, then —
 * behind only → fast-forward; diverged → refuse; aligned/ahead/no-remote →
 * nothing to do. Trustworthy because getAheadBehind THROWS on git failure
 * instead of reporting a fake 0/0. Call again AFTER the merge to re-sync local.
 */
export async function alignDevelopWithRemote(developWorktree: string, developBranch: string): Promise<DevelopAlignment> {
  const base = { worktree: developWorktree, ahead: 0, behind: 0, fastForwarded: false, diverged: false };

  const usable = await ensureWorkTreeUsable(developWorktree);
  if (!usable.ok) return { ...base, ok: false, error: usable.error };

  const fetched = await gitFetch('origin', developWorktree);
  if (fetched.exitCode !== 0) return { ...base, ok: false, error: `fetch origin failed: ${fetched.stderr || 'unknown error'}` };

  const remote = `origin/${developBranch}`;
  if (!(await remoteRefExists(remote, developWorktree))) return { ...base, ok: true };

  try {
    const { ahead, behind } = await getAheadBehind(developBranch, remote, developWorktree);
    const decision = decideAlignment({ ahead, behind, remoteExists: true });

    if (decision.action === 'diverged') {
      return { ...base, ahead, behind, diverged: true, ok: false,
        error: `Local '${developBranch}' has diverged from ${remote} (${ahead} ahead / ${behind} behind). Resolve manually before PR/merge.` };
    }
    if (decision.action === 'ff') {
      const ff = await fastForwardOnly(remote, developWorktree);
      if (ff.exitCode !== 0) {
        return { ...base, ahead, behind, ok: false, error: `fast-forward of '${developBranch}' to ${remote} failed: ${ff.stderr || 'unknown error'}` };
      }
      return { ...base, ahead, behind, fastForwarded: true, ok: true };
    }
    return { ...base, ahead, behind, ok: true };
  } catch (err) {
    return { ...base, ok: false, error: (err as Error).message };
  }
}

export async function cloneBare(url: string, bareDir: string): Promise<void> {
  mkdirSync(dirname(bareDir), { recursive: true });
  await execGitOrFail(['clone', '--bare', url, bareDir]);

  await execGitOrFail(['config', 'remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*'], bareDir);
  await execGit(['fetch', 'origin'], bareDir);
}

/**
 * Add a worktree for an EXISTING branch (init-time import of main/develop).
 * Unlike createWorktree (which always creates a NEW branch via `-b`), this
 * checks out the branch that already exists in the freshly-cloned bare repo:
 *   1. local head `refs/heads/<branch>` (bare clone copies all heads)  → checkout
 *   2. remote-tracking `refs/remotes/origin/<branch>`                  → create + track
 *   3. neither, but a fallbackBase is given (e.g. develop from main)   → branch off it
 * Throws if the branch cannot be resolved and no fallback is provided.
 */
export async function addWorktreeCheckout(
  bareDir: string,
  targetPath: string,
  branch: string,
  fallbackBase?: string,
): Promise<void> {
  mkdirSync(dirname(targetPath), { recursive: true });

  const localRef = await execGit(['show-ref', '--verify', `refs/heads/${branch}`], bareDir);
  if (localRef.exitCode === 0) {
    await execGitOrFail(['worktree', 'add', targetPath, branch], bareDir);
    return;
  }

  const remoteRef = await execGit(['show-ref', '--verify', `refs/remotes/origin/${branch}`], bareDir);
  if (remoteRef.exitCode === 0) {
    await execGitOrFail(['worktree', 'add', '-b', branch, targetPath, `origin/${branch}`], bareDir);
    return;
  }

  if (fallbackBase) {
    await execGitOrFail(['worktree', 'add', '-b', branch, targetPath, fallbackBase], bareDir);
    return;
  }

  throw new Error(`Branch '${branch}' not found in repository and no fallback base provided.`);
}

/** Default branch of a freshly-cloned bare repo (its HEAD symref), or null. */
export async function detectDefaultBranch(bareDir: string): Promise<string | null> {
  const res = await execGit(['symbolic-ref', '--short', 'HEAD'], bareDir);
  return res.exitCode === 0 && res.stdout ? res.stdout : null;
}

/**
 * Pick the MAINLINE branch for the 01-Main worktree: prefer `main`, then
 * `master`, and only fall back to the repo's HEAD default if neither exists.
 * Critically NOT the HEAD default by itself — many repos default to `develop`,
 * which must never be checked out in both the main AND develop worktrees.
 */
export async function resolveMainBranch(bareDir: string): Promise<string> {
  for (const candidate of ['main', 'master']) {
    const ref = await execGit(['show-ref', '--verify', `refs/heads/${candidate}`], bareDir);
    if (ref.exitCode === 0) return candidate;
  }
  return (await detectDefaultBranch(bareDir)) || 'main';
}
