import { Effect, Latch, Option } from "effect"
import * as EffectLogger from "@saeeol/core/effect/logger"
import * as Log from "@saeeol/core/util/log"
import { SessionID, MessageID } from "../core/schema"
import { MessageV2 } from "../message/message-v2"
import * as Session from "../core/session"
import { SaeeolSession } from "@/saeeol/session"
import { SaeeolSessionPrompt } from "@/saeeol/session/prompt"
import { SaeeolSessionPromptQueue } from "@/saeeol/session/prompt-queue"
import { SaeeolSessionProcessor } from "@/saeeol/session/processor"
import { InstanceState } from "@/effect/instance-state"
import { EffectBridge } from "@/effect/bridge"
import { type TaskPromptOps } from "@/tool/task"
import { NamedError } from "@saeeol/core/util/error"
import MAX_STEPS from "./max-steps.txt"
import { type PromptInput, type ShellInput, LoopInput, STRUCTURED_OUTPUT_SYSTEM_PROMPT, REQUEST_PRUNE_BYTES, createStructuredOutputTool } from "./prompt-schemas"
import type { PromptDeps } from "./prompt-types"
const elog = EffectLogger.create({ service: "session.prompt" })
const log = Log.create({ service: "session.prompt" })

export interface LoopHelpers {
  getModel: (...args: any[]) => any
  lastModel: (...args: any[]) => any
  lastAssistant: (...args: any[]) => any
  createUserMessage: (...args: any[]) => any
  ensureTitle: (...args: any[]) => any
  insertReminders: (...args: any[]) => any
  resolvePromptParts: (...args: any[]) => any
  commandHandler: (...args: any[]) => any
  prompt: (...args: any[]) => any
  cancel: (...args: any[]) => any
}

export function createRunLoop(deps: PromptDeps, helpers: LoopHelpers) {
  const closeReasons = new Map<string, any>()
  const resolveTools = (input: any) => deps.resolveToolsSvc.resolve(input)
  const runner = Effect.fn("SessionPrompt.runner")(function* () { return yield* EffectBridge.make() })
  const ops = Effect.fn("SessionPrompt.ops")(function* () {
    const run = yield* runner()
    return {
      cancel: (id: SessionID) => run.fork(helpers.cancel(id)),
      resolvePromptParts: (t: string) => helpers.resolvePromptParts(t),
      prompt: (input: PromptInput) => helpers.prompt(input),
    } satisfies TaskPromptOps
  })
  const handleSubtask = (input: any) => Effect.gen(function* () {
    const ops_ = yield* ops()
    return yield* deps.subtaskSvc.handle({ ...input, getModel: helpers.getModel, promptOps: ops_ })
  })
  const shellImpl = (input: ShellInput, ready?: Latch.Latch) =>
    deps.shellSvc.exec(input, helpers.getModel, helpers.lastModel as any, ready)
  const runLoop = Effect.fn("SessionPrompt.run")(function* (sessionID: SessionID) {
    const envCache: SaeeolSessionPrompt.EnvCache = {}
    closeReasons.delete(sessionID)
    let compactionAttempts = 0
    const ctx = yield* InstanceState.context
    const slog = elog.with({ sessionID })
    let structured: unknown | undefined
    let step = 0
    const session = yield* deps.sessions.get(sessionID)
    while (true) {
      yield* deps.status.set(sessionID, { type: "busy" })
      yield* slog.info("loop", { step })
      let msgs: MessageV2.WithParts[] = yield* MessageV2.filterCompactedEffect(sessionID)
      msgs = SaeeolSessionPromptQueue.scope(sessionID, msgs)
      msgs = SaeeolSessionPrompt.trimBeforeLastSummary(msgs)
      let lastUser: MessageV2.User | undefined, lastAssistant: MessageV2.Assistant | undefined, lastFinished: MessageV2.Assistant | undefined
      let tasks: (MessageV2.CompactionPart | MessageV2.SubtaskPart)[] = []
      for (let i = msgs.length - 1; i >= 0; i--) {
        const msg = msgs[i]
        if (!lastUser && msg.info.role === "user") lastUser = msg.info
        if (!lastAssistant && msg.info.role === "assistant") lastAssistant = msg.info
        if (!lastFinished && msg.info.role === "assistant" && msg.info.finish) lastFinished = msg.info
        if (lastUser && lastFinished) break
        const task = msg.parts.filter((part) => part.type === "compaction" || part.type === "subtask")
        if (task && !lastFinished) tasks.push(...task)
      }
      if (!lastUser) throw new Error("No user message found in stream.")
      const telemetry = SaeeolSessionProcessor.extractReviewTelemetry(
        msgs.findLast((m) => m.info.role === "user" && m.info.id === lastUser.id)?.parts ?? [],
      )
      const lastAssistantMsg = msgs.findLast(
        (msg) => msg.info.role === "assistant" && msg.info.id === lastAssistant?.id,
      )
      const hasToolCalls = lastAssistantMsg?.parts.some((part) => part.type === "tool" && !part.metadata?.providerExecuted) ?? false
      if (lastAssistant?.finish && hasToolCalls && lastAssistant.parentID === lastUser.id &&
        lastUser.id < lastAssistant.id &&
        SaeeolSessionPrompt.shouldAskPlanFollowup({ messages: msgs, abort: AbortSignal.any([]) })
      ) {
        const action = yield* Effect.promise((signal) =>
          SaeeolSessionPrompt.askPlanFollowup({ sessionID, messages: msgs, abort: signal }),
        )
        if (action === "continue") continue
        yield* slog.info("exiting loop")
        break
      }
      if (lastAssistant?.finish && !["tool-calls"].includes(lastAssistant.finish) &&
        !hasToolCalls && lastAssistant.parentID === lastUser.id && lastUser.id < lastAssistant.id
      ) {
        const action = yield* Effect.promise((signal) =>
          SaeeolSessionPrompt.askPlanFollowup({ sessionID, messages: msgs, abort: signal }),
        )
        if (action === "continue") continue
        yield* slog.info("exiting loop")
        break
      }
      step++
      if (step === 1)
        yield* helpers.ensureTitle({
          session, modelID: lastUser.model.modelID, providerID: lastUser.model.providerID, history: msgs,
        }).pipe(Effect.ignore, Effect.forkIn(deps.scope))
      const model = yield* helpers.getModel(lastUser.model.providerID, lastUser.model.modelID, sessionID)
      const task = tasks.pop()
      if (task?.type === "subtask") {
        yield* handleSubtask({ task, model, lastUser, sessionID, session, msgs })
        continue
      }
      if (task?.type === "compaction") {
        const result = yield* deps.compaction.process({
          messages: msgs, parentID: lastUser.id, sessionID, auto: task.auto, overflow: task.overflow,
        })
        if (result === "stop") { closeReasons.set(sessionID, "error"); break }
        continue
      }
      if (lastFinished && lastFinished.summary !== true &&
        (yield* deps.compaction.isOverflow({ tokens: lastFinished.tokens, model }))
      ) {
        const guard = SaeeolSessionPrompt.guardCompactionAttempt({
          sessionID, attempts: compactionAttempts, closeReasons, message: lastFinished,
        })
        if (guard.exhausted) {
          yield* deps.sessions.updateMessage(lastFinished)
          yield* deps.bus.publish(Session.Event.Error, { sessionID, error: guard.error })
          break
        }
        compactionAttempts++
        yield* deps.compaction.create({ sessionID, agent: lastUser.agent, model: lastUser.model, auto: true })
        continue
      }
      const agent = yield* deps.agents.get(lastUser.agent)
      if (!agent) {
        const available = (yield* deps.agents.list()).filter((a: any) => !a.hidden).map((a: any) => a.name)
        const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
        const error = new NamedError.Unknown({ message: `Agent not found: "${lastUser.agent}".${hint}` })
        yield* deps.bus.publish(Session.Event.Error, { sessionID, error: error.toObject() })
        throw error
      }
      const maxSteps = agent.steps ?? Infinity
      const isLastStep = step >= maxSteps
      msgs = yield* helpers.insertReminders({ messages: msgs, agent, session })
      const msg: MessageV2.Assistant = {
        id: MessageID.ascending(), parentID: lastUser.id, role: "assistant", mode: agent.name,
        agent: agent.name, variant: lastUser.model.variant, path: { cwd: ctx.directory, root: ctx.worktree },
        cost: 0, tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
        modelID: model.id, providerID: model.providerID, time: { created: Date.now() }, sessionID,
      }
      yield* deps.sessions.updateMessage(msg)
      const handle = yield* deps.processor.create({ assistantMessage: msg, sessionID, model, telemetry })
      const outcome: "break" | "continue" = yield* Effect.gen(function* () {
        const lastUserMsg = msgs.findLast((m) => m.info.role === "user")
        const bypassAgentCheck = lastUserMsg?.parts.some((p) => p.type === "agent") ?? false
        const tools = yield* resolveTools({
          agent, session, model, tools: lastUser.tools, processor: handle,
          bypassAgentCheck, messages: msgs,
        })
        if (lastUser.format?.type === "json_schema") {
          tools["StructuredOutput"] = createStructuredOutputTool({
            schema: lastUser.format.schema,
            onSuccess(output: unknown) { structured = output },
          })
        }
        if (step === 1)
          yield* deps.summary.summarize({ sessionID, messageID: lastUser.id }).pipe(Effect.ignore, Effect.forkIn(deps.scope))
        if (step > 1 && lastFinished) {
          for (const m of msgs) {
            if (m.info.role !== "user" || m.info.id <= lastFinished.id) continue
            for (const p of m.parts) {
              if (p.type !== "text" || p.ignored || p.synthetic || !p.text.trim()) continue
              p.text = ["<system-reminder>", "The user sent the following message:", p.text, "",
                "Please address this message and continue with your tasks.", "</system-reminder>"].join("\n")
            }
          }
        }
        yield* deps.plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
        SaeeolSessionPrompt.injectEditorContext({ msgs, lastUser, sessionID, cache: envCache })
        msgs = SaeeolSessionPrompt.maybeStripHistoricalMedia(msgs)
        const allResult: any = yield* Effect.all([
          deps.sys.skills(agent), deps.sys.environment(model, lastUser.editorContext),
          deps.instruction.system().pipe(Effect.orDie),
        ])
        const skills: any = allResult[0], env: any[] = allResult[1], instructions: any[] = allResult[2]
        let modelMsgs = yield* MessageV2.toModelMessagesEffect(msgs, model)
        const size = Buffer.byteLength(JSON.stringify(modelMsgs))
        if (size > REQUEST_PRUNE_BYTES) {
          yield* deps.compaction.prune({ sessionID, reason: "payload-limit" })
          msgs = yield* MessageV2.filterCompactedEffect(sessionID)
          msgs = SaeeolSessionPromptQueue.scope(sessionID, msgs)
          msgs = SaeeolSessionPrompt.trimBeforeLastSummary(msgs)
          yield* deps.plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
          SaeeolSessionPrompt.injectEditorContext({ msgs, lastUser, sessionID, cache: envCache })
          msgs = SaeeolSessionPrompt.maybeStripHistoricalMedia(msgs)
          modelMsgs = yield* MessageV2.toModelMessagesEffect(msgs, model)
          const nextSize = Buffer.byteLength(JSON.stringify(modelMsgs))
          if (nextSize > REQUEST_PRUNE_BYTES) log.warn("payload still large after pruning", { size: nextSize })
        }
        const system = [...env, ...instructions, ...(skills ? [skills] : [])]
        const format = lastUser.format ?? { type: "text" as const }
        if (format.type === "json_schema") system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT)
        const result = yield* handle.process({
          user: lastUser, agent, permission: SaeeolSessionPrompt.guardPermissions({ agent, session }),
          sessionID, parentSessionID: session.parentID, system,
          messages: [...modelMsgs, ...(isLastStep ? [{ role: "assistant" as const, content: MAX_STEPS }] : [])],
          tools, model, toolChoice: format.type === "json_schema" ? "required" : undefined,
        })
        if (structured !== undefined) {
          handle.message.structured = structured
          handle.message.finish = handle.message.finish ?? "stop"
          yield* deps.sessions.updateMessage(handle.message)
          return "break" as const
        }
        const finished = handle.message.finish && !["tool-calls", "unknown"].includes(handle.message.finish)
        if (finished && !handle.message.error) {
          if (format.type === "json_schema") {
            handle.message.error = new MessageV2.StructuredOutputError({
              message: "Model did not produce structured output", retries: 0,
            }).toObject()
            yield* deps.sessions.updateMessage(handle.message)
            return "break" as const
          }
          if (handle.message.finish === "error") {
            SaeeolSessionProcessor.providerFinishError(handle.message)
            yield* deps.sessions.updateMessage(handle.message)
            closeReasons.set(sessionID, "error")
            return "break" as const
          }
        }
        if (result === "stop") {
          if (handle.message.error) closeReasons.set(sessionID, "error")
          return "break" as const
        }
        if (result === "compact") {
          const guard = SaeeolSessionPrompt.guardCompactionAttempt({
            sessionID, attempts: compactionAttempts, closeReasons, message: handle.message,
          })
          if (guard.exhausted) {
            yield* deps.sessions.updateMessage(handle.message)
            yield* deps.bus.publish(Session.Event.Error, { sessionID, error: guard.error })
            return "break" as const
          }
          compactionAttempts++
          yield* deps.compaction.create({
            sessionID, agent: lastUser.agent, model: lastUser.model, auto: true,
            overflow: !handle.message.finish,
          })
        }
        if (SaeeolSessionPromptQueue.hasFollowup(sessionID)) {
          closeReasons.set(sessionID, "interrupted")
          return "break" as const
        }
        if (result !== "compact" && !handle.message.finish) {
          handle.message.finish = "unknown"
          yield* deps.sessions.updateMessage(handle.message)
        }
        return "continue" as const
      }).pipe(Effect.ensuring(deps.instruction.clear(handle.message.id)))
      if (outcome === "break") break
      continue
    }
    yield* deps.compaction.prune({ sessionID, reason: "normal" }).pipe(Effect.ignore, Effect.forkIn(deps.scope))
    return yield* helpers.lastAssistant(sessionID)
  })
  const loop = Effect.fn("SessionPrompt.loop")(function* (input: LoopInput) {
    yield* SaeeolSessionPrompt.recoverDanglingAssistant({
      sessionID: input.sessionID, status: deps.status, sessions: deps.sessions,
    })
    yield* SaeeolSessionPrompt.recoverProviderFinishError({
      sessionID: input.sessionID, status: deps.status, sessions: deps.sessions,
    })
    yield* deps.bus.publish(SaeeolSession.Event.TurnOpen, { sessionID: input.sessionID })
    return yield* Effect.onExit(
      deps.state.ensureRunning(input.sessionID, helpers.lastAssistant(input.sessionID), runLoop(input.sessionID)),
      Effect.fnUntraced(function* (exit) {
        yield* deps.bus.publish(SaeeolSession.Event.TurnClose, {
          sessionID: input.sessionID,
          reason: SaeeolSessionPrompt.resolveCloseReason({
            sessionID: input.sessionID, closeReasons, exit,
          }),
        })
      }),
    )
  })
  const shell = Effect.fn("SessionPrompt.shell")(function* (input: ShellInput) {
    const ready = yield* Latch.make()
    return yield* deps.state.startShell(
      input.sessionID, helpers.lastAssistant(input.sessionID), shellImpl(input, ready), ready,
    )
  })
  return { runLoop, loop, shell, closeReasons }
}
