import { Bus } from "../../bus"
import { BusEvent } from "../../bus/bus-event"
import { Identifier } from "../../id/id"
import { SessionID } from "../../session/schema"
import { zod as toZod } from "@opencode-ai/core/effect-zod"
import * as Log from "@opencode-ai/core/util/log"
import { Telemetry } from "@kilocode/kilo-telemetry"
import z from "zod"
import { Schema } from "effect"
import { KiloSessionPromptQueue } from "../session/prompt-queue"
import { Instance } from "../instance"
import { parseReviewCommand } from "../review/command"

export namespace Suggestion {
  const log = Log.create({ service: "suggestion" })

  export const Action = z
    .object({
      label: z.string().describe("Button or option label (1-5 words)"),
      description: z.string().optional().describe("Brief explanation of what this action does"),
      prompt: z.string().describe("Synthetic user prompt to inject when this action is accepted"),
    })
    .meta({
      ref: "SuggestionAction",
    })
  export type Action = z.infer<typeof Action>

  export const ActionSchema = Schema.Struct({
    label: Schema.String.annotate({ description: "Button or option label (1-5 words)" }),
    description: Schema.optional(Schema.String).annotate({
      description: "Brief explanation of what this action does",
    }),
    prompt: Schema.String.annotate({
      description: "Synthetic user prompt to inject when this action is accepted",
    }),
  })

  const SuggestionIDSchema = Schema.String.check(Schema.isStartsWith("sug"))
  const SuggestionID = toZod(SuggestionIDSchema)
  const SessionIDZod = toZod(SessionID)

  export const Info = z
    .object({
      text: z.string().describe("Suggestion text shown to the user"),
      actions: z.array(Action).min(1).max(2).describe("Available actions the user can take"),
    })
    .meta({
      ref: "SuggestionInfo",
    })
  export type Info = z.infer<typeof Info>

  export const Request = z
    .object({
      id: SuggestionID,
      sessionID: SessionIDZod,
      text: z.string().describe("Suggestion text shown to the user"),
      actions: z.array(Action).min(1).max(2).describe("Available actions the user can take"),
      blocking: z
        .boolean()
        .optional()
        .describe(
          "Whether this suggestion blocks prompt input. When unset, the TUI treats the suggestion as blocking for backwards compatibility; the built-in suggest tool always sets this to false.",
        ),
      tool: z
        .object({
          messageID: z.string(),
          callID: z.string(),
        })
        .optional(),
    })
    .meta({
      ref: "SuggestionRequest",
    })
  export type Request = z.infer<typeof Request>

  export const RequestSchema = Schema.Struct({
    id: SuggestionIDSchema,
    sessionID: SessionID,
    text: Schema.String,
    actions: Schema.Array(ActionSchema).check(Schema.isMinLength(1), Schema.isMaxLength(2)),
    blocking: Schema.optional(Schema.Boolean),
    tool: Schema.optional(
      Schema.Struct({
        messageID: Schema.String,
        callID: Schema.String,
      }),
    ),
  }).annotate({ identifier: "SuggestionRequest" })

  export const Accept = z.object({
    index: z.number().int().nonnegative().describe("Zero-based action index to accept"),
  })
  export type Accept = z.infer<typeof Accept>

  export const Event = {
    Shown: BusEvent.define("suggestion.shown", RequestSchema),
    Accepted: BusEvent.define(
      "suggestion.accepted",
      Schema.Struct({
        sessionID: SessionID,
        requestID: SuggestionIDSchema,
        index: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
        action: ActionSchema,
      }),
    ),
    Dismissed: BusEvent.define(
      "suggestion.dismissed",
      Schema.Struct({
        sessionID: SessionID,
        requestID: SuggestionIDSchema,
      }),
    ),
  }

  // (request IDs are globally unique so instance scoping is not needed)
  const pending: Record<
    string,
    {
      info: Request
      resolve: (action: Action) => void
      reject: (error: any) => void
    }
  > = {}

  export async function show(input: {
    sessionID: string
    text: string
    actions: Action[]
    blocking?: boolean
    tool?: { messageID: string; callID: string }
  }): Promise<Action> {
    // Auto-dismiss if a newer prompt is already queued on this session.
    // Synchronous check immediately before the pending set, so there's no
    // interleaving with dismissAll called from SessionPrompt.prompt.
    if (KiloSessionPromptQueue.hasFollowup(SessionID.make(input.sessionID))) {
      log.info("auto-dismissed — followup queued", { sessionID: input.sessionID })
      throw new DismissedError()
    }

    const s = { pending }
    const id = Identifier.ascending("suggestion")

    log.info("shown", { id, actions: input.actions.length })

    return new Promise<Action>((resolve, reject) => {
      const info: Request = {
        id,
        sessionID: SessionID.make(input.sessionID),
        text: input.text,
        actions: input.actions,
        blocking: input.blocking,
        tool: input.tool,
      }
      s.pending[id] = {
        info,
        resolve,
        reject,
      }
      info.actions.forEach((action, index) => {
        const cmd = parseReviewCommand(action.prompt)
        if (!cmd) return
        Telemetry.trackSuggestionShown({
          sessionId: info.sessionID,
          requestId: info.id,
          index,
          tool: "suggest",
          command: cmd,
          actionCount: info.actions.length,
        })
      })
      Bus.publish(Instance.current, Event.Shown, { ...info, sessionID: SessionID.make(info.sessionID) })
    })
  }

  export async function accept(input: { requestID: string; index: number }): Promise<boolean> {
    const s = { pending }
    const existing = s.pending[input.requestID]
    if (!existing) {
      log.warn("accept for unknown request", { requestID: input.requestID })
      return false
    }

    const action = existing.info.actions[input.index]
    if (!action) {
      log.warn("accept for invalid action index", { requestID: input.requestID, index: input.index })
      delete s.pending[input.requestID]
      existing.reject(new Error(`Invalid action index: ${input.index}`))
      return false
    }

    delete s.pending[input.requestID]

    log.info("accepted", { requestID: input.requestID, index: input.index, label: action.label })

    const cmd = parseReviewCommand(action.prompt)
    if (cmd) {
      Telemetry.trackSuggestionAccepted({
        sessionId: existing.info.sessionID,
        requestId: existing.info.id,
        index: input.index,
        tool: "suggest",
        command: cmd,
        actionCount: existing.info.actions.length,
      })
    }

    Bus.publish(Instance.current, Event.Accepted, {
      sessionID: SessionID.make(existing.info.sessionID),
      requestID: existing.info.id,
      index: input.index,
      action,
    })

    existing.resolve(action)
    return true
  }

  export async function dismiss(requestID: string): Promise<boolean> {
    const s = { pending }
    const existing = s.pending[requestID]
    if (!existing) {
      log.warn("dismiss for unknown request", { requestID })
      return false
    }
    delete s.pending[requestID]

    log.info("dismissed", { requestID })

    Bus.publish(Instance.current, Event.Dismissed, {
      sessionID: SessionID.make(existing.info.sessionID),
      requestID: existing.info.id,
    })

    existing.reject(new DismissedError())
    return true
  }

  export class DismissedError extends Error {
    constructor() {
      super("The user dismissed this suggestion")
    }
  }

  export async function dismissAll(sessionID: string): Promise<void> {
    const s = { pending }
    for (const [id, entry] of Object.entries(s.pending)) {
      if (entry.info.sessionID !== sessionID) continue
      delete s.pending[id]
      log.info("dismissed", { requestID: id })
      Bus.publish(Instance.current, Event.Dismissed, {
        sessionID: SessionID.make(entry.info.sessionID),
        requestID: entry.info.id,
      })
      entry.reject(new DismissedError())
    }
  }

  export async function list() {
    return Object.values(pending).map((item) => item.info)
  }
}
