import { Effect } from "effect"
import { applyEdits, modify } from "jsonc-parser"
import { AppFileSystem } from "@saeeol/core/filesystem"
import { Question } from "@/question"
import type { SessionID, MessageID, PartID } from "@/session/schema"
import { PartID as PartIDSchema } from "@/session/schema"
import type { MessageV2 } from "@/session/message-v2"
import { SaeeolConfig } from "@/saeeol/config/config"
import { ConfigParse } from "@/config/parse"
import * as Log from "@saeeol/core/util/log"
import { makeRuntime } from "@/effect/run-service"
import type { Config } from "@/config/config"
import { SaeeolSnapshotTrack as T } from "./track-types"

export namespace SaeeolSnapshotTrack {
  const log = Log.create({ service: "snapshot.track" })

  const questionRt = makeRuntime(Question.Service, Question.defaultLayer)
  const fsRt = makeRuntime(AppFileSystem.Service, AppFileSystem.defaultLayer)

  // Lazy to break a module-load cycle with @/session/index.ts.
  let cachedSessionRt: T.SessionRuntime | undefined
  async function sessionRuntime(): Promise<T.SessionRuntime> {
    if (cachedSessionRt) return cachedSessionRt
    const mod = await import("@/session/session")
    cachedSessionRt = makeRuntime(mod.Session.Service, mod.Session.defaultLayer) as unknown as T.SessionRuntime
    return cachedSessionRt
  }

  const progressPart = (input: {
    sessionID: SessionID
    messageID: MessageID
    partID: PartID
    text: string
  }): MessageV2.TextPart => ({
    id: input.partID,
    messageID: input.messageID,
    sessionID: input.sessionID,
    type: "text",
    text: input.text,
    synthetic: true,
  })

  export const defaultHooks: T.Hooks = {
    async startProgress(input) {
      const partID = PartIDSchema.ascending()
      try {
        const rt = await sessionRuntime()
        await rt.runPromise((svc) => svc.updatePart(progressPart({ ...input, partID })))
        return { sessionID: input.sessionID, messageID: input.messageID, partID }
      } catch (err) {
        log.warn("failed to publish snapshot progress part", { err })
        return undefined
      }
    },

    async updateProgress(input) {
      try {
        const rt = await sessionRuntime()
        await rt.runPromise((svc) =>
          svc.updatePart(
            progressPart({
              sessionID: input.handle.sessionID,
              messageID: input.handle.messageID,
              partID: input.handle.partID,
              text: input.text,
            }),
          ),
        )
      } catch (err) {
        log.warn("failed to update snapshot progress part", { err })
      }
    },

    async endProgress(input) {
      try {
        const rt = await sessionRuntime()
        await rt.runPromise((svc) =>
          svc.removePart({
            sessionID: input.handle.sessionID,
            messageID: input.handle.messageID,
            partID: input.handle.partID,
          }),
        )
      } catch (err) {
        log.warn("failed to remove snapshot progress part", { err })
      }
    },

    async ask(input) {
      const answers = await questionRt
        .runPromise((svc) =>
          svc.ask({
            sessionID: input.sessionID,
            blocking: true,
            questions: [
              {
                header: "Snapshot is slow",
                headerKey: "snapshot.slowRepo.header",
                question:
                  "It is taking a long time to initialize the snapshot system, likely due to the size of the repository.\n\n" +
                  "Do you want to disable Snapshots for this repository?",
                questionKey: "snapshot.slowRepo.question",
                custom: false,
                options: [
                  {
                    label: T.ANSWER_CONTINUE,
                    labelKey: "snapshot.slowRepo.answer.continue",
                    description:
                      "Keep waiting for the snapshot to complete. Subsequent turns are fast once the initial snapshot is built.",
                    descriptionKey: "snapshot.slowRepo.answer.continue.description",
                  },
                  {
                    label: T.ANSWER_DISABLE,
                    labelKey: "snapshot.slowRepo.answer.disable",
                    description:
                      "Turn off Saeeol's snapshots for this project. You will lose undo/redo of Saeeol file changes, but git still tracks everything.",
                    descriptionKey: "snapshot.slowRepo.answer.disable.description",
                  },
                ],
              },
            ],
          }),
        )
        .catch(() => undefined)
      const pick = answers?.[0]?.[0]
      if (pick === T.ANSWER_CONTINUE) return "continue"
      if (pick === T.ANSWER_DISABLE) return "disable"
      return "dismissed"
    },

    async persistDisable() {
      const directory = await currentDirectory()
      if (!directory) return
      const patch: Config.Info = { snapshot: false }
      await fsRt.runPromise((fs) =>
        Effect.gen(function* () {
          yield* SaeeolConfig.updateProjectConfig({
            fs,
            directory: directory.directory,
            worktree: directory.worktree,
            config: patch,
            read: (file: string) =>
              fs.readFileString(file).pipe(
                Effect.map((s) => s as string | undefined),
                Effect.catch(() => Effect.succeed<string | undefined>(undefined)),
              ),
            parse: (input: string, file: string) => ConfigParse.jsonc(input, file) as Config.Info,
            patch: patchTopLevelJsonc,
            writable: (c: Config.Info) => c,
          })
        }),
      )
    },
  }

  /**
   * Minimal JSONC patcher. Writes each top-level key in `patch` as a single
   * `modify()` edit — nested object values are replaced wholesale.
   */
  function patchTopLevelJsonc(input: string, patch: Config.Info): string {
    return Object.entries(patch).reduce((out, [key, value]) => {
      if (value === undefined) return out
      if (value !== null && typeof value === "object") {
        log.warn("patchTopLevelJsonc called with a non-scalar value", { key })
      }
      const edits = modify(out, [key], value, {
        formattingOptions: { insertSpaces: true, tabSize: 2 },
      })
      return applyEdits(out, edits)
    }, input)
  }

  async function currentDirectory(): Promise<{ directory: string; worktree?: string } | undefined> {
    const { Instance } = await import("@/project/instance")
    try {
      return { directory: Instance.directory, worktree: Instance.worktree }
    } catch {
      return undefined
    }
  }
}
