/**
 * Merge a workleaf into its parent (squash) and clean it up.
 *
 * Lifecycle:
 *   1. Resolve the target workleaf — explicit name positional OR auto-detect
 *      from cwd via detectWorkleafContext.
 *   2. Refuse if the workleaf has uncommitted changes.
 *   3. Verify the parent worktree still exists.
 *   4. Preflight a squash-merge probe in the parent (and bail cleanly
 *      on conflict — never leave the parent in MERGING state).
 *   5. Real `git merge --squash <branch>` in the parent.
 *   6. Commit with a generated message + Copilot co-author trailer.
 *   7. Cleanup: stop dev server, remove worktree, delete branch, release port.
 *
 * If we're inside the workleaf being merged, we move cwd to the parent
 * directory after merge so the user isn't left in a deleted directory.
 */

import * as p from '@clack/prompts'
import { execFileSync } from 'node:child_process'
import { existsSync } from 'node:fs'
import { resolve } from 'node:path'
import { repoRoot, worktreeDir } from '../worktree/port.js'
import {
  detectWorkleafContext,
  workleafBranchName,
  workleafDir,
  listWorkleafs,
  parentOfWorkleaf,
} from './paths.js'
import { cleanupWorkleaf, currentBranch, hasDirtyWorkingTree } from './cleanup.js'
import { dim, bold, green } from '../cli/intro.js'
import { parseFlags } from '../cli/flags.js'

const flagSchema = {
  message: { type: 'string', description: 'Override the squash commit message' },
  'no-commit': { type: 'boolean', default: false, description: 'Stage the squash but do not commit (caller commits)' },
  yes: { type: 'boolean', default: false, description: 'Skip interactive confirmation' },
}

const COAUTHOR_TRAILER = 'Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>'

/**
 * Locate the target workleaf by name (positional) OR by inspecting cwd.
 * Returns { parent, slug, branch, dir, root } or exits with an error.
 */
function resolveTarget(positionalName, root) {
  // Explicit positional wins
  if (positionalName) {
    // The user can pass either a slug OR a full branch name. Disambiguate.
    const branchInfo = parentOfWorkleaf(positionalName)
    if (branchInfo) {
      const dir = workleafDir(branchInfo.parent, branchInfo.slug, root)
      return {
        parent: branchInfo.parent,
        slug: branchInfo.slug,
        branch: positionalName,
        dir,
        root,
      }
    }
    // Treat as slug — find a matching workleaf, prefer one parented by
    // the current worktree if multiple match.
    const matches = listWorkleafs(root).filter((w) => w.slug === positionalName)
    if (matches.length === 0) {
      p.log.error(`No workleaf named "${positionalName}" found.`)
      p.log.info(`List workleafs with: ${green('npx storyboard workleaf list')}`)
      process.exit(1)
    }
    if (matches.length > 1) {
      p.log.error(`Multiple workleafs match "${positionalName}":`)
      for (const m of matches) console.log(`  ${m.branch}  ${dim(m.dir)}`)
      p.log.info(`Pass the full branch name: ${green('npx storyboard workleaf merge <parent>--workleaf--<slug>')}`)
      process.exit(1)
    }
    const m = matches[0]
    return { parent: m.parent, slug: m.slug, branch: m.branch, dir: m.dir, root }
  }

  // Auto-detect from cwd
  const ctx = detectWorkleafContext()
  if (!ctx.inWorkleaf) {
    p.log.error('Not inside a workleaf — pass a name or cd into one.')
    p.log.info(`Usage: ${green('npx storyboard workleaf merge <name>')}`)
    process.exit(1)
  }
  return { parent: ctx.parent, slug: ctx.slug, branch: ctx.branch, dir: ctx.dir, root }
}

/**
 * Preflight: probe whether `git merge --squash <branch>` in the parent
 * would succeed. We do the probe by running the actual merge command,
 * checking the result, and aborting the merge state if anything was
 * staged (i.e. we always leave the parent in its pre-probe state).
 *
 * Returns { ok: true } on clean merge OR { ok: false, conflicts: [...] }
 * on conflict. The parent is reset either way.
 */
function preflightSquash(parentDir, branch) {
  try {
    // --squash leaves the changes staged but does NOT create a merge commit
    // and does NOT set MERGE_HEAD, so on success we just need to reset.
    execFileSync('git', ['merge', '--squash', '--no-commit', branch], {
      cwd: parentDir,
      stdio: 'pipe',
    })
    // Reset the staged changes so the probe is non-destructive.
    execFileSync('git', ['reset', '--hard', 'HEAD'], { cwd: parentDir, stdio: 'pipe' })
    return { ok: true }
  } catch (err) {
    // Capture conflict markers, then reset state.
    let conflicts = []
    try {
      const out = execFileSync('git', ['diff', '--name-only', '--diff-filter=U'], {
        cwd: parentDir,
        encoding: 'utf8',
      }).trim()
      conflicts = out ? out.split('\n') : []
    } catch { /* ignore */ }
    // Always reset to leave the parent clean.
    try { execFileSync('git', ['reset', '--hard', 'HEAD'], { cwd: parentDir, stdio: 'pipe' }) } catch { /* ignore */ }
    return { ok: false, conflicts, error: err.message }
  }
}

export async function runMerge(argv = process.argv.slice(4)) {
  const { flags, positional } = parseFlags(argv, flagSchema)
  const root = repoRoot()
  const target = resolveTarget(positional[0], root)
  const { parent, slug, branch, dir } = target

  p.intro('storyboard workleaf merge')
  p.log.info(`${dim('workleaf:')} ${bold(branch)}`)

  // Verify workleaf exists on disk
  if (!existsSync(resolve(dir, '.git'))) {
    p.log.error(`Workleaf directory missing: ${dim(dir)}`)
    process.exit(1)
  }

  // Refuse on dirty workleaf
  if (hasDirtyWorkingTree(dir)) {
    p.log.error(`Workleaf has uncommitted changes — commit or stash them first.`)
    p.log.info(`  ${dim(`cd ${dir}`)}`)
    p.log.info(`  ${green('git status')}`)
    process.exit(1)
  }

  // Verify parent worktree
  const parentDir = parent === 'main' ? root : worktreeDir(parent, root)
  if (!existsSync(resolve(parentDir, '.git'))) {
    p.log.error(`Parent worktree "${parent}" is gone.`)
    p.log.info(`Recreate it (${green('npx storyboard branch ' + parent)}) and re-run, or abandon with`)
    p.log.info(`  ${green(`npx storyboard workleaf delete ${slug}`)}`)
    process.exit(1)
  }
  // Refuse if parent is on a different branch than expected — that
  // means the user is intentionally on a wildcard branch and probably
  // doesn't want a squash here.
  const parentBranch = currentBranch(parentDir)
  if (!parentBranch) {
    p.log.error(`Could not determine current branch of parent worktree at ${dim(parentDir)}`)
    process.exit(1)
  }
  if (hasDirtyWorkingTree(parentDir)) {
    p.log.error(`Parent worktree "${parent}" has uncommitted changes — commit or stash them first.`)
    p.log.info(`  ${dim(`cd ${parentDir}`)}`)
    p.log.info(`  ${green('git status')}`)
    process.exit(1)
  }

  // Preflight squash
  p.log.step('Checking that the squash will apply cleanly…')
  const probe = preflightSquash(parentDir, branch)
  if (!probe.ok) {
    p.log.error('Squash would conflict with the parent.')
    if (probe.conflicts && probe.conflicts.length > 0) {
      for (const c of probe.conflicts) console.log(`  - ${c}`)
    }
    p.log.info(`Resolve manually with:`)
    p.log.info(`  ${dim(`cd ${parentDir} && git merge --squash ${branch}`)}`)
    process.exit(1)
  }

  // Real merge
  p.log.step(`Squash-merging into ${bold(parentBranch)}`)
  try {
    execFileSync('git', ['merge', '--squash', branch], { cwd: parentDir, stdio: 'pipe' })
  } catch (err) {
    p.log.error(`Real squash failed (this shouldn't happen after a clean preflight): ${err.message}`)
    process.exit(1)
  }

  // Commit (or stage-only)
  let sha = null
  if (flags['no-commit']) {
    p.log.success(`Squash staged in ${bold(parentBranch)} — commit it yourself`)
  } else {
    const message = flags.message || `feat: ${slug} (workleaf squash)`
    try {
      execFileSync('git', ['commit', '-m', `${message}\n\n${COAUTHOR_TRAILER}`], {
        cwd: parentDir,
        stdio: 'pipe',
      })
      sha = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: parentDir, encoding: 'utf8' }).trim()
      p.log.success(`Committed as ${green(sha)}: ${dim(message)}`)
    } catch (err) {
      p.log.error(`Commit failed: ${err.message}`)
      p.log.warn(`Squash is staged in ${parentDir} but uncommitted. Resolve manually before re-running.`)
      process.exit(1)
    }
  }

  // CRITICAL: chdir to the parent BEFORE cleanup so cwd is not inside
  // the directory we're about to delete. Without this, every subsequent
  // call into helpers that touch cwd (releasePort -> portsFilePath ->
  // realpathSync) hits ENOENT.
  try { process.chdir(parentDir) } catch { /* parent may also be gone */ }

  p.log.step('Cleaning up workleaf')
  const { stopped } = cleanupWorkleaf({ branch, dir, root })

  p.note(
    [
      `  ${green('✓')} squashed into ${bold(parentBranch)}${sha ? ` as ${green(sha)}` : ''}`,
      `  ${green('✓')} stopped ${stopped} dev server${stopped === 1 ? '' : 's'}`,
      `  ${green('✓')} removed ${dim(dir.replace(root + '/', ''))}`,
      `  ${green('✓')} deleted branch ${dim(branch)}`,
      '',
      `  cd to the parent: ${green(`cd ${parentDir.replace(root + '/', '') || '.'}`)}`,
    ].join('\n'),
    `${bold(branch)} merged`
  )
  p.outro('')
}

// Re-export the branch-name helper for symmetry
export { workleafBranchName }
