import { Effect, Semaphore } from "effect"
import * as Log from "@saeeol/core/util/log"
import { DiffFull } from "@/saeeol/snapshot/diff-full"
import { locked, quoteFlags, mkArgs } from "./snapshot-git"
import type { GitResult } from "./snapshot-git"
import { ignore } from "./snapshot-core"
import type { GitCtx } from "./snapshot-core"
import type { FileDiff } from "./snapshot-types"

const log = Log.create({ service: "snapshot" })

export const diffOp = (ctx: GitCtx, addFn: () => Effect.Effect<void, any, any>) =>
  Effect.fnUntraced(function* (hash: string) {
    return yield* locked(ctx.locks, ctx.gitdir, Effect.gen(function* () {
      yield* addFn()
      const result = yield* ctx.git(
        [...quoteFlags, ...mkArgs(ctx.gitdir, ctx.worktree, ["diff", "--cached", "--no-ext-diff", hash, "--", "."])],
        { cwd: ctx.worktree },
      )
      if (result.code !== 0) {
        log.warn("failed to get diff", { hash, exitCode: result.code, stderr: result.stderr })
        return ""
      }
      return result.text.trim()
    }))
  })

export const diffFullOp = (ctx: GitCtx, addFn: () => Effect.Effect<void, any, any>) =>
  Effect.fnUntraced(function* (from: string, to: string) {
    return yield* locked(ctx.locks, ctx.gitdir, Effect.gen(function* () {
      const result: FileDiff[] = []
      const status = new Map<string, "added" | "deleted" | "modified">()

      const statuses = yield* ctx.git(
        [...quoteFlags, ...mkArgs(ctx.gitdir, ctx.worktree, ["diff", "--no-ext-diff", "--name-status", "--no-renames", from, to, "--", "."])],
        { cwd: ctx.directory },
      )
      for (const line of statuses.text.trim().split("\n")) {
        if (!line) continue
        const [code, file] = line.split("\t")
        if (!code || !file) continue
        status.set(file, code.startsWith("A") ? "added" : code.startsWith("D") ? "deleted" : "modified")
      }

      const numstat = yield* ctx.git(
        [...quoteFlags, ...mkArgs(ctx.gitdir, ctx.worktree, ["diff", "--no-ext-diff", "--no-renames", "--numstat", from, to, "--", "."])],
        { cwd: ctx.directory },
      )

      type Row = { file: string; status: "added" | "deleted" | "modified"; binary: boolean; additions: number; deletions: number }
      const rows: Row[] = numstat.text.trim().split("\n").filter(Boolean).flatMap((line) => {
        const [adds, dels, file] = line.split("\t")
        if (!file) return []
        const binary = adds === "-" && dels === "-"
        return [{
          file, status: status.get(file) ?? "modified" as const, binary,
          additions: binary ? 0 : (Number.isFinite(parseInt(adds)) ? parseInt(adds) : 0),
          deletions: binary ? 0 : (Number.isFinite(parseInt(dels)) ? parseInt(dels) : 0),
        }]
      })

      const ignored = yield* ignore(ctx)(rows.map((r) => r.file))
      if (ignored.size > 0) {
        const filtered = rows.filter((r) => !ignored.has(r.file))
        rows.length = 0
        rows.push(...filtered)
      }

      const step = 100
      for (let i = 0; i < rows.length; i += step) {
        const run = rows.slice(i, i + step)
        const patches = yield* DiffFull.batch(
          (cmd) => ctx.git([...quoteFlags, ...mkArgs(ctx.gitdir, ctx.worktree, cmd)], { cwd: ctx.directory }) as any,
          from, to, run.filter((r) => !r.binary).map((r) => r.file),
        )
        for (const row of run) {
          result.push({
            file: row.file, patch: row.binary ? "" : (patches.get(row.file) ?? ""),
            additions: row.additions, deletions: row.deletions, status: row.status,
          })
        }
      }
      return result
    }))
  })
