import { Effect, Layer, Duration, Schedule, Cause, Semaphore } from "effect"
import { ChildProcessSpawner } from "effect/unstable/process"
import path from "path"
import { makeRuntime } from "@/effect/run-service"
import { CrossSpawnSpawner } from "@saeeol/core/cross-spawn-spawner"
import { InstanceState } from "@/effect/instance-state"
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 { SaeeolSnapshotTrack } from "@/saeeol/snapshot/track"
import { Patch, Service, cache, maxCache, MAX_DIFF_SIZE, SummaryFileDiff, FileDiff } from "./snapshot-types"
import type { Interface } from "./snapshot-types"
import { mkGit } from "./snapshot-git"
import type { GitCtx } from "./snapshot-core"
import { cleanup as cleanupOp, track as trackOp, patch as patchOp, restore as restoreOp, revert as revertOp, add } from "./snapshot-core"
import { diffOp, diffFullOp } from "./snapshot-diff"

export { Patch, FileDiff, SummaryFileDiff, MAX_DIFF_SIZE, Service }
export type { Interface }
export type { Patch as PatchType, FileDiff as FileDiffType, SummaryFileDiff as SummaryFileDiffType }

type State = Omit<Interface, "init">

export const layer: Layer.Layer<
  Service,
  never,
  AppFileSystem.Service | ChildProcessSpawner.ChildProcessSpawner | Config.Service
> = Layer.effect(Service, Effect.gen(function* () {
  const fs = yield* AppFileSystem.Service
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
  const config = yield* Config.Service
  const locks = new Map<string, Semaphore.Semaphore>()

  const state = yield* InstanceState.make<State>(
    Effect.fn("Snapshot.state")(function* (ctx) {
      const gitCtx: GitCtx = {
        git: mkGit(spawner),
        gitdir: path.join(Global.Path.data, "snapshot", ctx.project.id, Hash.fast(ctx.worktree)),
        worktree: ctx.worktree,
        directory: ctx.directory,
        vcs: ctx.project.vcs ?? "",
        fs: {
          exists: (p: string) => fs.exists(p).pipe(Effect.orDie),
          readFileString: (p: string) => fs.readFileString(p).pipe(Effect.catch(() => Effect.succeed(""))),
          ensureDir: (p: string) => fs.ensureDir(p).pipe(Effect.orDie),
          writeFileString: (p: string, c: string) => fs.writeFileString(p, c).pipe(Effect.orDie),
          stat: (p: string) => fs.stat(p).pipe(Effect.catch(() => Effect.succeed({ type: "", size: 0 }))) as any,
          remove: (p: string) => fs.remove(p).pipe(Effect.catch(() => Effect.void)),
        },
        config: { get: () => config.get() },
        locks,
      }

      const addFn = add(gitCtx)
      const diffFn = diffOp(gitCtx, () => Effect.gen(function* () { yield* addFn() })) as any
      const diffFullFn = diffFullOp(gitCtx, () => Effect.gen(function* () { yield* addFn() })) as any

      yield* cleanupOp(gitCtx)().pipe(
        Effect.catchCause((cause) => Effect.logError("cleanup loop failed", { cause: Cause.pretty(cause) }).pipe(Effect.asVoid)),
        Effect.repeat(Schedule.spaced(Duration.hours(1))),
        Effect.delay(Duration.minutes(1)),
        Effect.forkScoped,
      )

      return {
        cleanup: cleanupOp(gitCtx),
        track: trackOp(gitCtx),
        patch: patchOp(gitCtx),
        restore: restoreOp(gitCtx),
        revert: revertOp(gitCtx),
        diff: diffFn,
        diffFull: diffFullFn,
      } as State
    }),
  )

  const trackState = SaeeolSnapshotTrack.makeState()
  return Service.of({
    init: Effect.fn("Snapshot.init")(function* () { yield* InstanceState.get(state) }),
    cleanup: Effect.fn("Snapshot.cleanup")(function* () { return yield* InstanceState.useEffect(state, (s) => s.cleanup()) }),
    track: Effect.fn("Snapshot.track")(function* (opts) {
      return yield* SaeeolSnapshotTrack.wrap({
        inner: InstanceState.useEffect(state, (s) => s.track()),
        state: trackState, sessionID: opts?.sessionID, messageID: opts?.messageID,
      })
    }),
    patch: Effect.fn("Snapshot.patch")(function* (hash) { return yield* InstanceState.useEffect(state, (s) => s.patch(hash)) }),
    restore: Effect.fn("Snapshot.restore")(function* (snapshot) { return yield* InstanceState.useEffect(state, (s) => s.restore(snapshot)) }),
    revert: Effect.fn("Snapshot.revert")(function* (patches) { return yield* InstanceState.useEffect(state, (s) => s.revert(patches)) }),
    diff: Effect.fn("Snapshot.diff")(function* (hash) { return yield* InstanceState.useEffect(state, (s) => s.diff(hash)) }),
    diffFull: Effect.fn("Snapshot.diffFull")(function* (from, to) {
      if (from === to) return []
      const key = `${from}:${to}`
      const hit = cache.get(key)
      if (hit) return yield* Effect.promise(() => hit)
      if (cache.size >= maxCache) {
        const first = cache.keys().next().value
        if (first) cache.delete(first)
      }
      const ctx = yield* Effect.context()
      const pending = Effect.runPromiseWith(ctx)(InstanceState.useEffect(state, (s) => s.diffFull(from, to))).catch((err) => { cache.delete(key); throw err })
      cache.set(key, pending)
      return yield* Effect.promise(() => pending)
    }),
  })
}))

export const defaultLayer = layer.pipe(
  Layer.provide(CrossSpawnSpawner.defaultLayer),
  Layer.provide(AppFileSystem.defaultLayer),
  Layer.provide(Config.defaultLayer),
)

const { runPromise } = makeRuntime(Service, defaultLayer)
export const track = () => runPromise((svc) => svc.track())
export const patch = (hash: string) => runPromise((svc) => svc.patch(hash))
export const restore = (snapshot: string) => runPromise((svc) => svc.restore(snapshot))
export const revert = (patches: Patch[]) => runPromise((svc) => svc.revert(patches))
export const diff = (hash: string) => runPromise((svc) => svc.diff(hash))
export const diffFull = (from: string, to: string) => runPromise((svc) => svc.diffFull(from, to))
export const cleanup = () => runPromise((svc) => svc.cleanup())
export const init = () => runPromise((svc) => svc.init())

export * as Snapshot from "."
