import * as vscode from "vscode"
import { GitOps } from "../../agent-manager/GitOps"
import { generatedLike } from "../../agent-manager/local-diff"
import { appendOutput, getWorkspaceRoot } from "../../review-utils"
import { imageMime, loadImage } from "../shared/image"
import { resolveInside } from "../shared/path"
import type { DiffFile } from "../types"
import type { DiffSource, DiffSourceDescriptor, DiffSourceFetch } from "./types"
import {
  blobOid,
  blobSize,
  INDEX_REF,
  MAX_DETAIL_BYTES,
  parseNameStatus,
  parseNumstat,
  parseRawOids,
  showBlob,
  showBlobBytes,
  summarize,
  type FileEntry,
} from "./git-status"

export const STAGED_SOURCE_ID = "staged"

export const STAGED_DESCRIPTOR: DiffSourceDescriptor = {
  id: STAGED_SOURCE_ID,
  type: "staged",
  group: "Git",
  capabilities: { revert: false, comments: true },
}

function stamp(entry: FileEntry, before: string, after: string): FileEntry {
  if (!imageMime(entry.file)) return entry
  return { ...entry, stamp: `${entry.status}:${before}:${after}` }
}

/**
 * Diff between the git index and HEAD — what `git diff --cached` would show.
 * Polls on the standard interval; revert isn't supported (use `git reset` from
 * a real git client). Read-only view.
 */
export function createStagedDiffSource(): DiffSource {
  const output = vscode.window.createOutputChannel("Kilo Diff: Staged")
  const log = (...args: unknown[]) => appendOutput(output, "StagedDiffSource", ...args)
  const git = new GitOps({ log })

  const root = (): string | undefined => getWorkspaceRoot()

  const listEntries = async (dir: string): Promise<FileEntry[]> => {
    const [nameStatus, numstat, raw] = await Promise.all([
      git.execGit(["-c", "core.quotepath=false", "diff", "--cached", "--name-status", "--no-renames", "HEAD"], dir),
      git.execGit(["-c", "core.quotepath=false", "diff", "--cached", "--numstat", "--no-renames", "HEAD"], dir),
      git.execGit(
        ["-c", "core.quotepath=false", "diff", "--cached", "--raw", "--abbrev=64", "--no-renames", "HEAD"],
        dir,
      ),
    ])
    if (nameStatus.code !== 0) {
      log("git diff --cached --name-status failed", { code: nameStatus.code, stderr: nameStatus.stderr.trim() })
      return []
    }
    const counts = parseNumstat(numstat.code === 0 ? numstat.stdout : "")
    const refs = parseRawOids(raw.code === 0 ? raw.stdout : "")
    return parseNameStatus(nameStatus.stdout).map((item) => {
      const ref = refs.get(item.file)
      const entry = {
        file: item.file,
        status: item.status,
        additions: counts.get(item.file)?.additions ?? 0,
        deletions: counts.get(item.file)?.deletions ?? 0,
        tracked: true,
        binary: counts.get(item.file)?.binary ?? false,
      }
      return stamp(
        entry,
        item.status === "added" ? "missing" : (ref?.before ?? "missing"),
        item.status === "deleted" ? "missing" : (ref?.after ?? "missing"),
      )
    })
  }

  return {
    descriptor: STAGED_DESCRIPTOR,

    async fetch(): Promise<DiffSourceFetch> {
      const dir = root()
      if (!dir) {
        log("No workspace root")
        return { diffs: [] }
      }
      const entries = await listEntries(dir)
      log(`Staged diff: ${entries.length} file(s)`)
      return { diffs: entries.map(summarize) }
    },

    async fetchFile(file: string): Promise<DiffFile | null> {
      const dir = root()
      if (!dir || !file || !resolveInside(dir, file)) return null

      // Resolve the entry for this single file so we know its status. Reading
      // both refs blindly would still work, but knowing the status lets us
      // skip impossible reads (e.g. HEAD: for an added file).
      const entry = await fileEntry(git, dir, file, log)
      if (!entry) return null

      const mime = imageMime(file)
      if (entry.binary && !mime) return summarize(entry)
      const beforeBytes = entry.status === "added" ? 0 : await blobSize(git, dir, "HEAD", file)
      const afterBytes = entry.status === "deleted" ? 0 : await blobSize(git, dir, INDEX_REF, file)
      if (mime) {
        const image = await loadImage(
          file,
          entry.status === "added"
            ? undefined
            : { bytes: beforeBytes, read: () => showBlobBytes(git, dir, "HEAD", file) },
          entry.status === "deleted"
            ? undefined
            : { bytes: afterBytes, read: () => showBlobBytes(git, dir, INDEX_REF, file) },
        )
        return { ...summarize(entry), summarized: false, image }
      }

      if (beforeBytes > MAX_DETAIL_BYTES || afterBytes > MAX_DETAIL_BYTES) {
        log("Staged detail skipped: file too large", { file, beforeBytes, afterBytes, cap: MAX_DETAIL_BYTES })
        return summarize(entry)
      }

      // For added: HEAD has no blob. For deleted: index has no blob.
      const before = entry.status === "added" ? "" : await showBlob(git, dir, "HEAD", file)
      const after = entry.status === "deleted" ? "" : await showBlob(git, dir, INDEX_REF, file)
      const result = await git.execGit(
        ["-c", "core.quotepath=false", "diff", "--cached", "--no-ext-diff", "--no-renames", "HEAD", "--", file],
        dir,
      )
      const patch = result.code === 0 ? result.stdout : undefined
      const summarized = before === "" && after === "" && entry.status === "modified"
      return {
        file,
        before,
        after,
        patch,
        additions: entry.additions,
        deletions: entry.deletions,
        status: entry.status,
        tracked: true,
        generatedLike: generatedLike(file),
        summarized,
        stamp: entry.stamp ?? `${entry.status}:${entry.additions}:${entry.deletions}`,
      }
    },

    dispose(): void {
      git.dispose()
      output.dispose()
    },
  }
}

async function fileEntry(
  git: GitOps,
  dir: string,
  file: string,
  log: (...args: unknown[]) => void,
): Promise<FileEntry | undefined> {
  const result = await git.execGit(
    ["-c", "core.quotepath=false", "diff", "--cached", "--name-status", "--no-renames", "HEAD", "--", file],
    dir,
  )
  if (result.code !== 0) {
    log("Single-file staged status lookup failed", { file, stderr: result.stderr.trim() })
    return undefined
  }
  const items = parseNameStatus(result.stdout)
  const item = items[0]
  if (!item) return undefined

  const counts = await git.execGit(
    ["-c", "core.quotepath=false", "diff", "--cached", "--numstat", "--no-renames", "HEAD", "--", file],
    dir,
  )
  const stats = parseNumstat(counts.code === 0 ? counts.stdout : "")
  const entry = {
    file: item.file,
    status: item.status,
    additions: stats.get(item.file)?.additions ?? 0,
    deletions: stats.get(item.file)?.deletions ?? 0,
    tracked: true,
    binary: stats.get(item.file)?.binary ?? false,
  }
  if (!imageMime(item.file)) return entry
  const [before, after] = await Promise.all([
    item.status === "added" ? "missing" : blobOid(git, dir, "HEAD", item.file),
    item.status === "deleted" ? "missing" : blobOid(git, dir, INDEX_REF, item.file),
  ])
  return stamp(entry, before, after)
}
