/** @jsxImportSource @opentui/solid */
import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
import { createMemo, createSignal } from "solid-js"
import { appendFileSync } from "fs"
import { processPoll } from "./src/stats.ts"

const DEBUG_ENABLED = false
const DEBUG_LOG = "/tmp/oc-ls-stats-debug.log"
function debug(...args: unknown[]) {
  if (!DEBUG_ENABLED) return
  try {
    const line = `${args.map(a => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" ")}\n`
    appendFileSync(DEBUG_LOG, line)
  } catch {}
}

type StreamSample = {
  at: number
  tokens: number
}

const STREAM_WINDOW_MS = 5_000
const SLOT_POLL_MS = 500
type MessageTiming = {
  sessionID: string
  requestStartAt: number
  firstResponseAt?: number
  firstTokenAt?: number
  lastTokenAt?: number
}

 type TrackerState = {
  streamSamplesBySession: Record<string, StreamSample[]>
  messageTimingByID: Record<string, MessageTiming>
  lastPrefillRate: number
  lastGeneratedTps: number
  isPrefilling: boolean
  prefillSlotId: number | null
  prefillCapturedTokens: number | null
  prefillStartAt: number
  isGenerating: boolean
  generateSlotId: number | null
  generatePrevNd: number
  generateStartAt: number
  prevNdBySlot: Record<number, { prevNd: number; idTask: number; classification: "TG" | "PP" | "neither" }>
  prevPromptTokensBySlot: Record<number, { tokens: number; idTask: number }>
  failure: string | null
}

function estimateStreamTokens(delta: string) {
  return Math.max(1, Math.ceil(Buffer.byteLength(delta, "utf8") / 5))
}

function stripBaseUrlPath(baseURL: string): string {
  try {
    const url = new URL(baseURL)
    return `${url.origin}`
  } catch {
    return baseURL.replace(/\/v1(\/.*)?$/, "").replace(/\/$/, "") || baseURL
  }
}

function extractProviderUrls(config: unknown): string[] {
  if (!config || typeof config !== "object") { debug("extractProviderUrls: config is null/undefined or not object"); return [] }
  const obj = config as Record<string, unknown>
  const provider = obj.provider
  if (!provider || typeof provider !== "object") { debug("extractProviderUrls: no provider in config"); return [] }

  const urls: string[] = []
  let total = 0
  let matched = 0
  for (const [key, val] of Object.entries(provider as Record<string, unknown>)) {
    total++
    if (val && typeof val === "object") {
      const opts = (val as Record<string, unknown>).options
      if (opts && typeof opts === "object") {
        const baseURL = (opts as Record<string, unknown>).baseURL || (opts as Record<string, unknown>).base_url
        debug(`extractProviderUrls: provider="${key}" baseURL=${baseURL}`)
        if (typeof baseURL === "string" && key.toLowerCase().includes("llama") && !key.toLowerCase().includes("ollama")) {
          urls.push(stripBaseUrlPath(baseURL))
          matched++
          debug(`extractProviderUrls: MATCH provider="${key}" -> ${urls[urls.length-1]}`)
        } else {
          debug(`extractProviderUrls: SKIP provider="${key}" (llama=${key.toLowerCase().includes("llama")} ollama=${key.toLowerCase().includes("ollama")})`)
        }
      }
    }
  }
  debug(`extractProviderUrls: total=${total} matched=${matched} urls=${JSON.stringify(urls)}`)
  return urls
}

async function loadConfigUrls(api: Parameters<TuiPlugin>[0]): Promise<string[]> {
  try {
    return extractProviderUrls(api.state.config as unknown)
  } catch (e) {
    return []
  }
}

async function fetchSlots(baseUrl: string, model?: string): Promise<unknown | null> {
  try {
    const url = model ? `${baseUrl}/slots?model=${encodeURIComponent(model)}` : `${baseUrl}/slots`
    const resp = await fetch(url)
    if (!resp.ok) return null
    return await resp.json()
  } catch {
    return null
  }
}

function formatPps(value: number) {
  if (!Number.isFinite(value) || value <= 0) return undefined
  return `${Math.round(value)}`
}

function formatTps(value: number) {
  if (!Number.isFinite(value) || value <= 0) return undefined
  return `${Math.round(value)}`
}

function SessionPromptRight(props: {
  api: Parameters<TuiPlugin>[0]
  sessionID: string
  tracker: TrackerState
  version: () => number
  clock: () => number
}) {
  const liveTps = createMemo(() => {
    props.version()
    props.clock()
    const rate = props.tracker.lastGeneratedTps
    if (!Number.isFinite(rate) || rate <= 0) return undefined
    return rate
  })

  const prefillRate = createMemo(() => {
    props.version()
    if (!props.tracker.isPrefilling) return undefined
    const rate = props.tracker.lastPrefillRate
    if (!Number.isFinite(rate) || rate <= 0) return undefined
    return rate
  })

  const text = createMemo(() => {
    if (props.tracker.failure) return props.tracker.failure
    const pp = prefillRate()
    const tps = liveTps()
    if (props.tracker.isPrefilling && pp) {
      return `${formatPps(pp)} tps (PP)`
    }
    if (props.tracker.isGenerating && tps) {
      return `${formatTps(tps)} tps (TG)`
    }
    return `- tps (TG)`
  })

  return <>{text() ? <text fg={props.api.theme.current.textMuted}>{text()}</text> : null}</>
}

const tui: TuiPlugin = async (api) => {
  console.log("[oc-ls-stats] TUI plugin loaded!")
  const tracker: TrackerState = {
    streamSamplesBySession: {},
    messageTimingByID: {},
    lastPrefillRate: 0,
    lastGeneratedTps: 0,
    isPrefilling: false,
    prefillSlotId: null,
    prefillCapturedTokens: null,
    prefillStartAt: 0,
    isGenerating: false,
    generateSlotId: null,
    generatePrevNd: 0,
    generateStartAt: 0,
    prevNdBySlot: {},
    prevPromptTokensBySlot: {},
    failure: null,
  }
  const [version, setVersion] = createSignal(0)
  const [clock, setClock] = createSignal(Date.now())

  const bump = () => setVersion((value) => value + 1)

  const pruneSamples = (now = Date.now()) => {
    let changed = false

    for (const [sessionID, samples] of Object.entries(tracker.streamSamplesBySession)) {
      const next = samples.filter((sample) => now - sample.at <= STREAM_WINDOW_MS)
      if (next.length !== samples.length) {
        changed = true
        if (next.length > 0) tracker.streamSamplesBySession[sessionID] = next
        else delete tracker.streamSamplesBySession[sessionID]
      }
    }

    if (changed) bump()
  }

  const clearLiveSamples = (sessionID: string) => {
    if (!tracker.streamSamplesBySession[sessionID]?.length) return
    delete tracker.streamSamplesBySession[sessionID]
    bump()
  }

  const appendSample = (sessionID: string, messageID: string, sample: StreamSample) => {
    const now = sample.at
    tracker.streamSamplesBySession[sessionID] = [
      ...(tracker.streamSamplesBySession[sessionID] ?? []).filter((item) => now - item.at <= STREAM_WINDOW_MS),
      sample,
    ]
    const timing = tracker.messageTimingByID[messageID]
    if (timing) {
      tracker.messageTimingByID[messageID] = timing.firstTokenAt
        ? { ...timing, lastTokenAt: now }
        : {
            ...timing,
            firstResponseAt: timing.firstResponseAt ?? now,
            firstTokenAt: now,
            lastTokenAt: now,
          }
    }
    bump()
  }

  let llamaServerUrls: string[] = []
  let llamaServerModel: string | undefined

  const pollMetrics = async () => {
    if (!llamaServerUrls.length) return

    let model = llamaServerModel
    if (!model) {
      const routeSessionID = api.route.current.params?.sessionID
      if (routeSessionID) {
        const session = api.state.session.get(routeSessionID)
        if (session?.model?.id) {
          model = session.model.id
        }
      }
    }

   debug(`pollMetrics: urls=${JSON.stringify(llamaServerUrls)} model=${model}`)
    for (const baseUrl of llamaServerUrls) {
      debug(`pollMetrics: polling ${baseUrl}`)
      const slots = await fetchSlots(baseUrl, model)
      debug(`pollMetrics: ${baseUrl} response=${JSON.stringify(slots)}`)
      const slotList = Array.isArray(slots) ? slots : slots ? Object.values(slots) : []
      debug(`pollMetrics: ${baseUrl} slotList.length=${slotList.length}`)
      if (slotList.length === 0) {
        tracker.failure = "n/a"
        bump()
        continue
      }
      tracker.failure = null
      bump()

      const result = processPoll(slotList, tracker)

      if (result.isPrefilling) {
        debug(`PP rate=${Math.round(result.lastPrefillRate)} tps`)
      }
      if (result.isGenerating) {
        debug(`TG rate=${Math.round(result.lastGeneratedTps)} tps`)
      }
      if (result.ppEnded) {
        debug(`PP ended rate=${Math.round(result.lastPrefillRate)} tps`)
      }
      if (result.tgEnded) {
        debug(`TG ended rate=${Math.round(result.lastGeneratedTps)} tps`)
      }
    }

    if (tracker.lastPrefillRate > 0 || tracker.lastGeneratedTps > 0) {
      bump()
    }
  }

  const onDelta = api.event.on("message.part.delta", (evt) => {
    if (evt.properties.field !== "text") return
    const parts = api.state.part(evt.properties.messageID)
    const part = parts.find((item) => item.id === evt.properties.partID)
    if (!part) return
    if (part.type !== "text" && part.type !== "reasoning") return
    appendSample(evt.properties.sessionID, evt.properties.messageID, {
      at: Date.now(),
      tokens: estimateStreamTokens(evt.properties.delta),
    })
  })

  const onMessage = api.event.on("message.updated", (evt) => {
    if (evt.properties.info.role !== "assistant") return

    if (!evt.properties.info.time.completed) {
      const existing = tracker.messageTimingByID[evt.properties.info.id]
      tracker.messageTimingByID[evt.properties.info.id] = {
        sessionID: evt.properties.sessionID,
        requestStartAt: evt.properties.info.time.created,
        firstResponseAt: existing?.firstResponseAt,
        firstTokenAt: existing?.firstTokenAt,
        lastTokenAt: existing?.lastTokenAt,
      }
      bump()
      return
    }

    delete tracker.messageTimingByID[evt.properties.info.id]
    pruneSamples(evt.properties.info.time.completed)
    bump()
  })

  const onPart = api.event.on("message.part.updated", (evt) => {
    if (evt.properties.part.type !== "tool") return
    if (
      evt.properties.part.state.status === "running" ||
      evt.properties.part.state.status === "completed" ||
      evt.properties.part.state.status === "error"
    ) {
      clearLiveSamples(evt.properties.sessionID)
    }
    const timing = tracker.messageTimingByID[evt.properties.part.messageID]
    if (!timing) return
    if (evt.properties.part.state.status === "pending") {
      tracker.messageTimingByID[evt.properties.part.messageID] = {
        ...timing,
        firstResponseAt: timing.firstResponseAt ?? evt.properties.time,
      }
      bump()
      return
    }
    if (evt.properties.part.state.status !== "running") return
    bump()
  })

  const timer = setInterval(() => {
    setClock(Date.now())
    pruneSamples()
  }, 1000)

  const slotTimer = setInterval(async () => {
    await pollMetrics()
  }, SLOT_POLL_MS)

  api.lifecycle.onDispose(() => {
    onDelta()
    onMessage()
    onPart()
    clearInterval(timer)
    clearInterval(slotTimer)
  })

  try {
    const configUrls = await loadConfigUrls(api)
    llamaServerUrls = configUrls.length > 0 ? configUrls : ["http://localhost:8080"]
  } catch (e) {
    console.error("[oc-ls-stats] loadConfigUrls error:", e)
    llamaServerUrls = ["http://localhost:8080"]
  }

  api.slots.register({
    slots: {
      session_prompt_right(_ctx, value) {
        return <SessionPromptRight api={api} sessionID={value.session_id} tracker={tracker} version={version} clock={clock} />
      },
    },
  })
}

const plugin: TuiPluginModule & { id: string } = {
  id: "@troed/oc-ls-stats",
  tui,
}

export default plugin
