import * as git from '../lib/git.js'
import * as branch from '../lib/branch.js'
import * as config from '../lib/config.js'
import * as worktree from '../lib/worktree.js'
import * as version from '../lib/version.js'
import * as provider from '../lib/provider.js'
import type { FinishResult, FinishOptions } from './types.js'

/**
 * Resolve a working directory in which `branchName` is checked out, so we can
 * operate on it (merge-back, version bump) WITHOUT a fragile `git checkout`
 * that fails when the branch lives in another worktree.
 *   - worktree repo  → that branch's worktree path (no checkout)
 *   - flat repo      → check the branch out in `cwd` and use `cwd`
 * Returns undefined when the branch can be neither located nor checked out.
 */
async function resolveBranchWorkdir(branchName: string, cwd?: string): Promise<string | undefined> {
  const worktrees = await worktree.listWorktrees(cwd)
  const wt = worktrees.find((w) => w.branch === branchName)
  if (wt?.path) return wt.path
  try {
    await git.checkout(branchName, cwd)
    return cwd ?? process.cwd()
  } catch {
    return undefined
  }
}

export async function execute(
  opts: FinishOptions,
  cwd?: string,
): Promise<FinishResult> {
  try {
    // Get current or specified branch
    const currentBranch = opts.branch || await git.getCurrentBranch(cwd)

    // Detect type
    const branchType = branch.detectBranchType(currentBranch)

    // Block if main/develop/other
    if (branchType === 'main' || branchType === 'develop' || branchType === 'other') {
      return {
        success: false,
        branch: currentBranch,
        branchType: 'feature',
        error: `Cannot finish ${branchType} branch`,
      }
    }

    // Read config
    const cfg = await config.readConfigForPlatform()
    const warnings: string[] = []

    // Correct target by type (feature→develop, release/hotfix→main) + invariant.
    const targetBranch = branch.getTargetBranch(branchType, cfg)
    branch.assertBranchTarget(branchType, targetBranch, cfg)

    // Confirm the branch is merged. Prefer the provider's PR state (robust to
    // squash-merges, which `git branch --merged` cannot detect); fall back to git
    // against the CORRECT target.
    let merged = false
    try {
      const pr = await provider.findPR(currentBranch, cfg.git.provider, cwd)
      if (pr && /merged|completed|closed/i.test(pr.state)) merged = true
    } catch {
      // provider/CLI unavailable — fall through to the git check
    }
    if (!merged) {
      try {
        merged = await git.isMerged(currentBranch, targetBranch, cwd)
      } catch {
        // ignore — treated as "not confirmed"
      }
    }

    if (!merged) {
      // 4.x behaviour: WARN, do not hard-block. But skip every destructive step
      // (tag, merge-back, branch deletion) so we never tag prematurely or destroy
      // unmerged work. The user merges the PR, then runs finish again.
      warnings.push(
        `Could not confirm ${currentBranch} is merged into ${targetBranch}. ` +
        `Skipped tag / merge-back / branch deletion — merge the PR first, then run finish again.`,
      )
      return {
        success: true,
        branch: currentBranch,
        branchType: branchType as 'feature' | 'release' | 'hotfix',
        warnings,
      }
    }

    let tag: string | undefined
    let mergedBack = false
    let versionBumped = false
    let newVersion: string | undefined

    // For release/hotfix: tag the released main commit, update develop FROM main
    // (merge-back), then bump develop to the next dev version and COMMIT + PUSH
    // it. Mirrors the 4.x finish (step-finish + finish-version-bumping) exactly.
    if (branchType === 'release' || branchType === 'hotfix') {
      const versionStr = (currentBranch.match(/(?:release|hotfix)\/(.+)/)?.[1] || '').trim()

      if (!/^\d+\.\d+\.\d+/.test(versionStr)) {
        warnings.push(
          `Could not parse a semantic version from "${currentBranch}" — skipped tag / merge-back / version bump.`,
        )
      } else {
        await git.fetch('origin', cwd)
        const mainBranch = cfg.git.branches.main
        const developBranch = cfg.git.branches.develop
        const tagName = `${cfg.versioning.tagPrefix}${versionStr}`

        // 1. Tag the RELEASED main commit (origin/main) — no checkout (worktree-safe).
        if (await git.tagExists(tagName, cwd)) {
          tag = tagName
          warnings.push(`Tag ${tagName} already exists — not re-created.`)
        } else if (await git.branchExists(mainBranch, true, cwd)) {
          await git.createTagAt(tagName, `origin/${mainBranch}`, `Release ${versionStr} (${branchType})`, cwd)
          await git.pushTag(tagName, cwd)
          tag = tagName
        } else {
          warnings.push(`Remote ${mainBranch} not found — skipped tagging (merge the PR into ${mainBranch} first).`)
        }

        // 2. Update develop FROM main (merge origin/main → develop) in develop's
        //    own worktree, so no fragile checkout is needed.
        const devDir = await resolveBranchWorkdir(developBranch, cwd)
        if (!devDir) {
          warnings.push(
            `Could not locate a "${developBranch}" worktree/checkout — skipped merge-back and version bump. ` +
            `Update ${developBranch} from ${mainBranch} manually.`,
          )
        } else {
          await git.pull('origin', developBranch, devDir)
          const merge = await git.mergeRefNoFf(
            `origin/${mainBranch}`,
            `Merge ${mainBranch} into ${developBranch} after ${versionStr}`,
            devDir,
          )
          if (!merge.ok) {
            await git.abortMerge(devDir)
            warnings.push(
              `Merge ${mainBranch} → ${developBranch} hit conflicts (${merge.conflicts.join(', ') || 'unknown'}) — ` +
              `aborted. Resolve manually, then re-run finish.`,
            )
          } else {
            const pushDev = await git.push(developBranch, devDir)
            mergedBack = pushDev.exitCode === 0
            if (!mergedBack) {
              warnings.push(`Merge-back ${mainBranch} → ${developBranch} committed but push failed: ${pushDev.stderr || pushDev.stdout}`)
            }

            // 3. Bump develop to the next dev version, then COMMIT + PUSH (4.x).
            newVersion = version.nextDevVersion(versionStr, branchType)
            const bump = await version.writeAndCommitVersion(
              devDir,
              newVersion,
              cfg.versioning.sources,
              `chore: bump version to ${newVersion} for development`,
            )
            if (bump.committed) {
              const pushBump = await git.push(developBranch, devDir)
              versionBumped = pushBump.exitCode === 0
              if (!versionBumped) {
                warnings.push(`Version bump committed but push failed: ${pushBump.stderr || pushBump.stdout}`)
              }
            } else if (bump.error) {
              warnings.push(`Version bump commit failed: ${bump.error}`)
            } else {
              warnings.push(`${developBranch} already at ${newVersion} — no version bump needed.`)
            }
          }
        }
      }
    }

    // Cleanup worktree if enabled
    let worktreeCleaned = false
    if (cfg.worktrees.enabled) {
      const wtPath = worktree.getWorktreePath(currentBranch, cfg)
      if (wtPath) {
        try {
          await worktree.removeWorktree(wtPath, cwd)
          worktreeCleaned = true
        } catch {
          // Ignore worktree cleanup errors
        }
      }
    }

    // Delete branch locally and remotely
    try {
      await git.deleteBranch(currentBranch, false, false, cwd)
    } catch {
      // Ignore local deletion errors
    }

    try {
      await git.deleteBranch(currentBranch, true, false, cwd)
    } catch {
      // Ignore remote deletion errors
    }

    return {
      success: true,
      branch: currentBranch,
      branchType: branchType as 'feature' | 'release' | 'hotfix',
      tag,
      mergedBack,
      versionBumped,
      version: newVersion,
      worktreeCleaned,
      warnings: warnings.length > 0 ? warnings : undefined,
    }
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err)
    return {
      success: false,
      branch: opts.branch || '',
      branchType: 'feature',
      error: message,
    }
  }
}
