import { Effect, Semaphore } from "effect"
import path from "path"
import { AppFileSystem } from "@saeeol/core/filesystem"
import { Hash } from "@saeeol/core/util/hash"
import { Config } from "@/config/config"
import { Global } from "@saeeol/core/global"
import * as Log from "@saeeol/core/util/log"
import { Flag } from "@saeeol/core/flag/flag"
import { InstanceState } from "@/effect/instance-state"
import { mkGit, mkArgs, feed, locked, coreFlags, cfgFlags, quoteFlags, limit } from "./snapshot-git"
import type { GitResult, Spawner } from "./snapshot-git"
import type { Patch } from "./snapshot-types"

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

type GitFn = (cmd: string[], opts?: { cwd?: string; env?: Record<string, string>; stdin?: any }) => Effect.Effect<GitResult, any, any>
type FsSvc = {
  exists: (p: string) => Effect.Effect<boolean, never, never>
  readFileString: (p: string) => Effect.Effect<string, never, never>
  ensureDir: (p: string) => Effect.Effect<void, never, never>
  writeFileString: (p: string, c: string) => Effect.Effect<void, never, never>
  stat: (p: string) => Effect.Effect<{ type: string; size: number | bigint }, never, never>
  remove: (p: string) => Effect.Effect<void, never, never>
}
type CfgSvc = {
  get: () => Effect.Effect<{ snapshot?: boolean }>
}

export interface GitCtx {
  git: GitFn
  gitdir: string
  worktree: string
  directory: string
  vcs: string
  fs: FsSvc
  config: CfgSvc
  locks: Map<string, Semaphore.Semaphore>
}

const doIgnore = (ctx: GitCtx) =>
  Effect.fnUntraced(function* (files: string[]) {
    if (!files.length) return new Set<string>()
    const check = yield* ctx.git(
      [...quoteFlags, "--git-dir", path.join(ctx.worktree, ".git"), "--work-tree", ctx.worktree, "check-ignore", "--no-index", "--stdin", "-z"],
      { cwd: ctx.directory, stdin: feed(files) },
    )
    if (check.code !== 0 && check.code !== 1) return new Set<string>()
    return new Set(check.text.split("\0").filter(Boolean))
  })

export { doIgnore as ignore }

const doDrop = (ctx: GitCtx) =>
  Effect.fnUntraced(function* (files: string[]) {
    if (!files.length) return
    yield* ctx.git(
      [...cfgFlags, ...mkArgs(ctx.gitdir, ctx.worktree, ["rm", "--cached", "-f", "--ignore-unmatch", "--pathspec-from-file=-", "--pathspec-file-nul"])],
      { cwd: ctx.directory, stdin: feed(files) },
    )
  })

const doStage = (ctx: GitCtx) =>
  Effect.fnUntraced(function* (files: string[]) {
    if (!files.length) return
    const result = yield* ctx.git(
      [...cfgFlags, ...mkArgs(ctx.gitdir, ctx.worktree, ["add", "--all", "--sparse", "--pathspec-from-file=-", "--pathspec-file-nul"])],
      { cwd: ctx.directory, stdin: feed(files) },
    )
    if (result.code === 0) return
    log.warn("failed to add snapshot files", { exitCode: result.code, stderr: result.stderr })
  })

const doEnabled = (ctx: GitCtx) =>
  Effect.fnUntraced(function* () {
    if (ctx.vcs !== "git") return false
    if (Flag.SAEEOL_CLIENT === "acp") return false
    return (yield* ctx.config.get()).snapshot !== false
  })

const doExcludes = (ctx: GitCtx) =>
  Effect.fnUntraced(function* () {
    const result = yield* ctx.git(["rev-parse", "--path-format=absolute", "--git-path", "info/exclude"], { cwd: ctx.worktree })
    const file = result.text.trim()
    if (!file) return
    if (!(yield* ctx.fs.exists(file))) return
    return file
  })

const doSync = (ctx: GitCtx) =>
  Effect.fnUntraced(function* (list: string[] = []) {
    const file = yield* doExcludes(ctx)()
    const target = path.join(ctx.gitdir, "info", "exclude")
    const text = [
      file ? (yield* ctx.fs.readFileString(file)).trimEnd() : "",
      ...list.map((item) => `/${item.replaceAll("\\", "/")}`),
    ].filter(Boolean).join("\n")
    yield* ctx.fs.ensureDir(path.join(ctx.gitdir, "info"))
    yield* ctx.fs.writeFileString(target, text ? `${text}\n` : "")
  })

export const add = (ctx: GitCtx) =>
  Effect.fnUntraced(function* () {
    yield* doSync(ctx)()
    const [diff, other] = yield* Effect.all(
      [
        ctx.git([...quoteFlags, ...mkArgs(ctx.gitdir, ctx.worktree, ["diff-files", "--name-only", "-z", "--", "."])], { cwd: ctx.directory }),
        ctx.git([...quoteFlags, ...mkArgs(ctx.gitdir, ctx.worktree, ["ls-files", "--others", "--exclude-standard", "-z", "--", "."])], { cwd: ctx.directory }),
      ],
      { concurrency: 2 },
    )
    if (diff.code !== 0 || other.code !== 0) {
      log.warn("failed to list snapshot files", { diffCode: diff.code, diffStderr: diff.stderr, otherCode: other.code, otherStderr: other.stderr })
      return
    }
    const tracked = diff.text.split("\0").filter(Boolean)
    const untracked = other.text.split("\0").filter(Boolean)
    const all = Array.from(new Set([...tracked, ...untracked]))
    if (!all.length) return
    const ignored = yield* doIgnore(ctx)(all)
    if (ignored.size > 0) {
      log.info("removing gitignored files from snapshot", { count: ignored.size })
      yield* doDrop(ctx)(Array.from(ignored))
    }
    const allow = all.filter((item) => !ignored.has(item))
    if (!allow.length) return
    const large = new Set(
      (yield* Effect.all(
        allow.map((item) =>
          ctx.fs.stat(path.join(ctx.directory, item))
            .pipe(Effect.catch(() => Effect.void))
            .pipe(Effect.map((stat) => {
              if (!stat || stat.type !== "File") return
              const size = typeof stat.size === "bigint" ? Number(stat.size) : stat.size
              return size > limit ? item : undefined
            }))
        ),
        { concurrency: 8 },
      )).filter((item): item is string => Boolean(item)),
    )
    const block = new Set(untracked.filter((item) => large.has(item)))
    yield* doSync(ctx)(Array.from(block))
    yield* doStage(ctx)(allow.filter((item) => !block.has(item)))
  })

export const cleanup = (ctx: GitCtx) =>
  Effect.fnUntraced(function* () {
    return yield* locked(ctx.locks, ctx.gitdir, Effect.gen(function* () {
      if (!(yield* doEnabled(ctx)())) return
      if (!(yield* ctx.fs.exists(ctx.gitdir))) return
      const result = yield* ctx.git(mkArgs(ctx.gitdir, ctx.worktree, ["gc", `--prune=${prune}`]), { cwd: ctx.directory })
      if (result.code !== 0) { log.warn("cleanup failed", { exitCode: result.code, stderr: result.stderr }); return }
      log.info("cleanup", { prune })
    }))
  })

export const track = (ctx: GitCtx) =>
  Effect.fnUntraced(function* () {
    return yield* locked(ctx.locks, ctx.gitdir, Effect.gen(function* () {
      if (!(yield* doEnabled(ctx)())) return
      const existed = yield* ctx.fs.exists(ctx.gitdir)
      yield* ctx.fs.ensureDir(ctx.gitdir)
      if (!existed) {
        yield* ctx.git(["init"], { env: { GIT_DIR: ctx.gitdir, GIT_WORK_TREE: ctx.worktree } })
        yield* ctx.git(["--git-dir", ctx.gitdir, "config", "core.autocrlf", "false"])
        yield* ctx.git(["--git-dir", ctx.gitdir, "config", "core.longpaths", "true"])
        yield* ctx.git(["--git-dir", ctx.gitdir, "config", "core.symlinks", "true"])
        yield* ctx.git(["--git-dir", ctx.gitdir, "config", "core.fsmonitor", "false"])
        log.info("initialized")
      }
      yield* add(ctx)()
      const result = yield* ctx.git(mkArgs(ctx.gitdir, ctx.worktree, ["write-tree"]), { cwd: ctx.directory })
      const hash = result.text.trim()
      log.info("tracking", { hash, cwd: ctx.directory, git: ctx.gitdir })
      return hash
    }))
  })

export const patch = (ctx: GitCtx) =>
  Effect.fnUntraced(function* (hash: string) {
    return yield* locked(ctx.locks, ctx.gitdir, Effect.gen(function* () {
      yield* add(ctx)()
      const result = yield* ctx.git(
        [...quoteFlags, ...mkArgs(ctx.gitdir, ctx.worktree, ["diff", "--cached", "--no-ext-diff", "--name-only", hash, "--", "."])],
        { cwd: ctx.directory },
      )
      if (result.code !== 0) { log.warn("failed to get diff", { hash, exitCode: result.code }); return { hash, files: [] } }
      const files = result.text.trim().split("\n").map((x) => x.trim()).filter(Boolean)
      const ignored = yield* doIgnore(ctx)(files)
      return { hash, files: files.filter((item) => !ignored.has(item)).map((x) => path.join(ctx.worktree, x).replaceAll("\\", "/")) }
    }))
  })

export const restore = (ctx: GitCtx) =>
  Effect.fnUntraced(function* (snapshot: string) {
    return yield* locked(ctx.locks, ctx.gitdir, Effect.gen(function* () {
      log.info("restore", { commit: snapshot })
      const result = yield* ctx.git([...coreFlags, ...mkArgs(ctx.gitdir, ctx.worktree, ["read-tree", snapshot])], { cwd: ctx.worktree })
      if (result.code === 0) {
        const checkout = yield* ctx.git([...coreFlags, ...mkArgs(ctx.gitdir, ctx.worktree, ["checkout-index", "-a", "-f"])], { cwd: ctx.worktree })
        if (checkout.code === 0) return
        log.error("failed to restore snapshot", { snapshot, exitCode: checkout.code, stderr: checkout.stderr })
        return
      }
      log.error("failed to restore snapshot", { snapshot, exitCode: result.code, stderr: result.stderr })
    }))
  })

export const revert = (ctx: GitCtx) =>
  Effect.fnUntraced(function* (patches: Patch[]) {
    return yield* locked(ctx.locks, ctx.gitdir, Effect.gen(function* () {
      const ops: { hash: string; file: string; rel: string }[] = []
      const seen = new Set<string>()
      for (const item of patches) {
        for (const file of item.files) {
          if (seen.has(file)) continue
          seen.add(file)
          ops.push({ hash: item.hash, file, rel: path.relative(ctx.worktree, file).replaceAll("\\", "/") })
        }
      }
      const single = Effect.fnUntraced(function* (op: (typeof ops)[number]) {
        log.info("reverting", { file: op.file, hash: op.hash })
        const result = yield* ctx.git([...coreFlags, ...mkArgs(ctx.gitdir, ctx.worktree, ["checkout", op.hash, "--", op.file])], { cwd: ctx.worktree })
        if (result.code === 0) return
        const tree = yield* ctx.git([...coreFlags, ...mkArgs(ctx.gitdir, ctx.worktree, ["ls-tree", op.hash, "--", op.rel])], { cwd: ctx.worktree })
        if (tree.code === 0 && tree.text.trim()) {
          log.info("file existed in snapshot but checkout failed, keeping", { file: op.file, hash: op.hash })
          return
        }
        log.info("file did not exist in snapshot, deleting", { file: op.file, hash: op.hash })
        yield* ctx.fs.remove(op.file)
      })
      const clash = (a: string, b: string) => a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`)
      for (let i = 0; i < ops.length;) {
        const first = ops[i]!
        const run = [first]
        let j = i + 1
        while (j < ops.length && run.length < 100) {
          const next = ops[j]!
          if (next.hash !== first.hash) break
          if (run.some((item) => clash(item.rel, next.rel))) break
          run.push(next)
          j += 1
        }
        if (run.length === 1) { yield* single(first); i = j; continue }
        const tree = yield* ctx.git(
          [...coreFlags, ...mkArgs(ctx.gitdir, ctx.worktree, ["ls-tree", "--name-only", first.hash, "--", ...run.map((item) => item.rel)])],
          { cwd: ctx.worktree },
        )
        if (tree.code !== 0) {
          log.info("batched ls-tree failed, falling back to single-file revert", { hash: first.hash, files: run.length })
          for (const op of run) yield* single(op)
          i = j
          continue
        }
        const have = new Set(tree.text.trim().split("\n").map((item) => item.trim()).filter(Boolean))
        const list = run.filter((item) => have.has(item.rel))
        if (list.length) {
          log.info("reverting", { hash: first.hash, files: list.length })
          const result = yield* ctx.git(
            [...coreFlags, ...mkArgs(ctx.gitdir, ctx.worktree, ["checkout", first.hash, "--", ...list.map((item) => item.file)])],
            { cwd: ctx.worktree },
          )
          if (result.code !== 0) {
            log.info("batched checkout failed, falling back to single-file revert", { hash: first.hash, files: list.length })
            for (const op of run) yield* single(op)
            i = j
            continue
          }
        }
        for (const op of run) {
          if (have.has(op.rel)) continue
          log.info("file did not exist in snapshot, deleting", { file: op.file, hash: op.hash })
          yield* ctx.fs.remove(op.file)
        }
        i = j
      }
    }))
  })
