import { AppRuntime } from "@/effect/app-runtime"
import { Git } from "@/git"
import { Instance } from "@/project/instance"
import { Process } from "@/util/process"
import { AGENT_USERNAME } from "./github-types"

export type GitOps = {
  text: (args: string[]) => Promise<string>
  run: (args: string[]) => Promise<void>
  status: (args: string[]) => Promise<{ exitCode: number; stdout: { toString(): string }; stderr: { toString(): string } }>
  commit: (summary: string, actor?: string) => Promise<void>
}

export function createGitOps(): GitOps {
  const exec = async (args: string[]) =>
    AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree })))

  const text = async (args: string[]) => {
    const result = await exec(args)
    if (result.exitCode !== 0) {
      throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
    }
    return result.text().trim()
  }

  const run = async (args: string[]) => {
    const result = await exec(args)
    if (result.exitCode !== 0) {
      throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
    }
  }

  const status = (args: string[]) => exec(args)

  const commit = async (summary: string, actor?: string) => {
    const args = ["commit", "-m", summary]
    if (actor) args.push("-m", `Co-authored-by: ${actor} <${actor}@users.noreply.github.com>`)
    await run(args)
  }

  return { text, run, status, commit }
}

export async function configureGit(git: GitOps, appToken: string, isMock: boolean) {
  if (isMock) return undefined
  console.log("Configuring git...")
  const config = "http.https://github.com/.extraheader"
  const ret = await git.status(["config", "--local", "--get", config])
  let saved: string | undefined
  if (ret.exitCode === 0) {
    saved = ret.stdout.toString().trim()
    await git.run(["config", "--local", "--unset-all", config])
  }

  const newCredentials = Buffer.from(`x-access-token:${appToken}`, "utf8").toString("base64")

  await git.run(["config", "--local", config, `AUTHORIZATION: basic ${newCredentials}`])
  await git.run(["config", "--global", "user.name", AGENT_USERNAME])
  await git.run(["config", "--global", "user.email", `${AGENT_USERNAME}@users.noreply.github.com`])
  return saved
}

export async function restoreGitConfig(git: GitOps, saved: string | undefined) {
  if (saved === undefined) return
  await git.run(["config", "--local", "http.https://github.com/.extraheader", saved])
}

function generateBranchName(type: "issue" | "pr" | "schedule" | "dispatch", issueId?: number) {
  const timestamp = new Date()
    .toISOString()
    .replace(/[:-]/g, "")
    .replace(/\.\d{3}Z/, "")
    .split("T")
    .join("")
  if (type === "schedule" || type === "dispatch") {
    const hex = crypto.randomUUID().slice(0, 6)
    return `saeeol/${type}-${hex}-${timestamp}`
  }
  return `saeeol/${type}${issueId}-${timestamp}`
}

export async function checkoutNewBranch(git: GitOps, type: "issue" | "schedule" | "dispatch", issueId?: number) {
  console.log("Checking out new branch...")
  const branch = generateBranchName(type, issueId)
  await git.run(["checkout", "-b", branch])
  return branch
}

export async function checkoutLocalBranch(
  git: GitOps,
  pr: { headRefName: string; commits: { totalCount: number } },
) {
  console.log("Checking out local branch...")

  const branch = pr.headRefName
  const depth = Math.max(pr.commits.totalCount, 20)

  await git.run(["fetch", "origin", `--depth=${depth}`, branch])
  await git.run(["checkout", branch])
}

export async function checkoutForkBranch(
  git: GitOps,
  pr: { headRefName: string; headRepository: { nameWithOwner: string }; commits: { totalCount: number } },
  issueId: number,
) {
  console.log("Checking out fork branch...")

  const remoteBranch = pr.headRefName
  const localBranch = generateBranchName("pr", issueId)
  const depth = Math.max(pr.commits.totalCount, 20)

  await git.run(["remote", "add", "fork", `https://github.com/${pr.headRepository.nameWithOwner}.git`])
  await git.run(["fetch", "fork", `--depth=${depth}`, remoteBranch])
  await git.run(["checkout", "-b", localBranch, `fork/${remoteBranch}`])
  return localBranch
}

export async function pushToNewBranch(
  git: GitOps,
  summary: string,
  branch: string,
  shouldCommit: boolean,
  actor?: string,
) {
  console.log("Pushing to new branch...")
  if (shouldCommit) {
    await git.run(["add", "."])
    await git.commit(summary, actor)
  }
  await git.run(["push", "-u", "origin", branch])
}

export async function pushToLocalBranch(git: GitOps, summary: string, shouldCommit: boolean, actor?: string) {
  console.log("Pushing to local branch...")
  if (shouldCommit) {
    await git.run(["add", "."])
    await git.commit(summary, actor)
  }
  await git.run(["push"])
}

export async function pushToForkBranch(
  git: GitOps,
  summary: string,
  pr: { headRefName: string },
  shouldCommit: boolean,
  actor?: string,
) {
  console.log("Pushing to fork branch...")

  const remoteBranch = pr.headRefName

  if (shouldCommit) {
    await git.run(["add", "."])
    await git.commit(summary, actor)
  }
  await git.run(["push", "fork", `HEAD:${remoteBranch}`])
}

export async function branchIsDirty(git: GitOps, originalHead: string, expectedBranch: string) {
  console.log("Checking if branch is dirty...")
  const current = await git.text(["rev-parse", "--abbrev-ref", "HEAD"])
  if (current !== expectedBranch) {
    console.log(`Branch changed during chat: expected ${expectedBranch}, now on ${current}`)
    return { dirty: true, uncommittedChanges: false, switched: true }
  }

  const ret = await git.status(["status", "--porcelain"])
  const status = ret.stdout.toString().trim()
  if (status.length > 0) {
    return { dirty: true, uncommittedChanges: true, switched: false }
  }
  const head = await git.text(["rev-parse", "HEAD"])
  return {
    dirty: head !== originalHead,
    uncommittedChanges: false,
    switched: false,
  }
}

export async function hasNewCommits(git: GitOps, base: string, head: string) {
  const result = await git.status(["rev-list", "--count", `${base}..${head}`])
  if (result.exitCode !== 0) {
    console.log(`rev-list failed, fetching origin/${base}...`)
    await git.status(["fetch", "origin", base, "--depth=1"])
    const retry = await git.status(["rev-list", "--count", `origin/${base}..${head}`])
    if (retry.exitCode !== 0) return true
    return parseInt(retry.stdout.toString().trim()) > 0
  }
  return parseInt(result.stdout.toString().trim()) > 0
}
