import {
  type Message,
  type Agent,
  type Session,
  type Part,
  type Config,
  type Path,
  type Project,
  type FileDiff,
  type Todo,
  type SessionStatus,
  type ProviderListResponse,
  type ProviderAuthResponse,
  type Command,
  type McpStatus,
  type LspStatus,
  type VcsInfo,
  type PermissionRequest,
  type QuestionRequest,
  createOpencodeClient,
} from "@opencode-ai/sdk/v2/client"
import { createStore, produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
import { Binary } from "@opencode-ai/util/binary"
import { retry } from "@opencode-ai/util/retry"
import { useGlobalSDK } from "./global-sdk"
import { ErrorPage, type InitError } from "../pages/error"
import {
  batch,
  createContext,
  createEffect,
  getOwner,
  runWithOwner,
  useContext,
  onCleanup,
  onMount,
  type Accessor,
  type ParentProps,
  Switch,
  Match,
} from "solid-js"
import { showToast } from "@opencode-ai/ui/toast"
import { getFilename } from "@opencode-ai/util/path"
import { usePlatform } from "./platform"
import { Persist, persisted } from "@/utils/persist"

type State = {
  status: "loading" | "partial" | "complete"
  agent: Agent[]
  command: Command[]
  project: string
  provider: ProviderListResponse
  config: Config
  path: Path
  session: Session[]
  sessionTotal: number
  session_status: {
    [sessionID: string]: SessionStatus
  }
  session_diff: {
    [sessionID: string]: FileDiff[]
  }
  todo: {
    [sessionID: string]: Todo[]
  }
  permission: {
    [sessionID: string]: PermissionRequest[]
  }
  question: {
    [sessionID: string]: QuestionRequest[]
  }
  mcp: {
    [name: string]: McpStatus
  }
  lsp: LspStatus[]
  vcs: VcsInfo | undefined
  limit: number
  message: {
    [sessionID: string]: Message[]
  }
  part: {
    [messageID: string]: Part[]
  }
}

type VcsCache = {
  store: Store<{ value: VcsInfo | undefined }>
  setStore: SetStoreFunction<{ value: VcsInfo | undefined }>
  ready: Accessor<boolean>
}

type ChildOptions = {
  bootstrap?: boolean
}

function createGlobalSync() {
  const globalSDK = useGlobalSDK()
  const platform = usePlatform()
  const owner = getOwner()
  if (!owner) throw new Error("GlobalSync must be created within owner")
  const vcsCache = new Map<string, VcsCache>()
  const [globalStore, setGlobalStore] = createStore<{
    ready: boolean
    error?: InitError
    path: Path
    project: Project[]
    provider: ProviderListResponse
    provider_auth: ProviderAuthResponse
    config: Config
    reload: undefined | "pending" | "complete"
  }>({
    ready: false,
    path: { state: "", config: "", worktree: "", directory: "", home: "" },
    project: [],
    provider: { all: [], connected: [], default: {} },
    provider_auth: {},
    config: {},
    reload: undefined,
  })
  let bootstrapQueue: string[] = []

  createEffect(async () => {
    if (globalStore.reload !== "complete") return
    if (bootstrapQueue.length) {
      for (const directory of bootstrapQueue) {
        bootstrapInstance(directory)
      }
      bootstrap()
    }
    bootstrapQueue = []
    setGlobalStore("reload", undefined)
  })

  const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
  const booting = new Map<string, Promise<void>>()
  const sessionLoads = new Map<string, Promise<void>>()
  const sessionMeta = new Map<string, { limit: number }>()

  function ensureChild(directory: string) {
    if (!directory) console.error("No directory provided")
    if (!children[directory]) {
      const cache = runWithOwner(owner, () =>
        persisted(
          Persist.workspace(directory, "vcs", ["vcs.v1"]),
          createStore({ value: undefined as VcsInfo | undefined }),
        ),
      )
      if (!cache) throw new Error("Failed to create persisted cache")
      vcsCache.set(directory, { store: cache[0], setStore: cache[1], ready: cache[3] })

      const init = () => {
        children[directory] = createStore<State>({
          project: "",
          provider: { all: [], connected: [], default: {} },
          config: {},
          path: { state: "", config: "", worktree: "", directory: "", home: "" },
          status: "loading" as const,
          agent: [],
          command: [],
          session: [],
          sessionTotal: 0,
          session_status: {},
          session_diff: {},
          todo: {},
          permission: {},
          question: {},
          mcp: {},
          lsp: [],
          vcs: cache[0].value,
          limit: 5,
          message: {},
          part: {},
        })
      }

      runWithOwner(owner, init)
    }
    const childStore = children[directory]
    if (!childStore) throw new Error("Failed to create store")
    return childStore
  }

  function child(directory: string, options: ChildOptions = {}) {
    const childStore = ensureChild(directory)
    const shouldBootstrap = options.bootstrap ?? true
    if (shouldBootstrap && childStore[0].status === "loading") {
      void bootstrapInstance(directory)
    }
    return childStore
  }

  async function loadSessions(directory: string) {
    const pending = sessionLoads.get(directory)
    if (pending) return pending

    const [store, setStore] = child(directory, { bootstrap: false })
    const meta = sessionMeta.get(directory)
    if (meta && meta.limit >= store.limit) return

    const promise = globalSDK.client.session
      .list({ directory, roots: true })
      .then((x) => {
        const nonArchived = (x.data ?? [])
          .filter((s) => !!s?.id)
          .filter((s) => !s.time?.archived)
          .slice()
          .sort((a, b) => a.id.localeCompare(b.id))

        // Read the current limit at resolve-time so callers that bump the limit while
        // a request is in-flight still get the expanded result.
        const limit = store.limit

        const sandboxWorkspace = globalStore.project.some((p) => (p.sandboxes ?? []).includes(directory))
        if (sandboxWorkspace) {
          setStore("sessionTotal", nonArchived.length)
          setStore("session", reconcile(nonArchived, { key: "id" }))
          sessionMeta.set(directory, { limit })
          return
        }

        const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
        // Include up to the limit, plus any updated in the last 4 hours
        const sessions = nonArchived.filter((s, i) => {
          if (i < limit) return true
          const updated = new Date(s.time?.updated ?? s.time?.created).getTime()
          return updated > fourHoursAgo
        })
        // Store total session count (used for "load more" pagination)
        setStore("sessionTotal", nonArchived.length)
        setStore("session", reconcile(sessions, { key: "id" }))
        sessionMeta.set(directory, { limit })
      })
      .catch((err) => {
        console.error("Failed to load sessions", err)
        const project = getFilename(directory)
        showToast({ title: `Failed to load sessions for ${project}`, description: err.message })
      })

    sessionLoads.set(directory, promise)
    promise.finally(() => {
      sessionLoads.delete(directory)
    })
    return promise
  }

  async function bootstrapInstance(directory: string) {
    if (!directory) return
    const pending = booting.get(directory)
    if (pending) return pending

    const promise = (async () => {
      const [store, setStore] = ensureChild(directory)
      const cache = vcsCache.get(directory)
      if (!cache) return
      const sdk = createOpencodeClient({
        baseUrl: globalSDK.url,
        fetch: platform.fetch,
        directory,
        throwOnError: true,
      })

      setStore("status", "loading")

      createEffect(() => {
        if (!cache.ready()) return
        const cached = cache.store.value
        if (!cached?.branch) return
        setStore("vcs", (value) => value ?? cached)
      })

      const blockingRequests = {
        project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
        provider: () =>
          sdk.provider.list().then((x) => {
            const data = x.data!
            setStore("provider", {
              ...data,
              all: data.all.map((provider) => ({
                ...provider,
                models: Object.fromEntries(
                  Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated"),
                ),
              })),
            })
          }),
        agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
        config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
      }

      try {
        await Promise.all(Object.values(blockingRequests).map((p) => retry(p)))
      } catch (err) {
        console.error("Failed to bootstrap instance", err)
        const project = getFilename(directory)
        const message = err instanceof Error ? err.message : String(err)
        showToast({ title: `Failed to reload ${project}`, description: message })
        setStore("status", "partial")
        return
      }

      if (store.status !== "complete") setStore("status", "partial")

      Promise.all([
        sdk.path.get().then((x) => setStore("path", x.data!)),
        sdk.command.list().then((x) => setStore("command", x.data ?? [])),
        sdk.session.status().then((x) => setStore("session_status", x.data!)),
        loadSessions(directory),
        sdk.mcp.status().then((x) => setStore("mcp", x.data!)),
        sdk.lsp.status().then((x) => setStore("lsp", x.data!)),
        sdk.vcs.get().then((x) => {
          const next = x.data ?? store.vcs
          setStore("vcs", next)
          if (next?.branch) cache.setStore("value", next)
        }),
        sdk.permission.list().then((x) => {
          const grouped: Record<string, PermissionRequest[]> = {}
          for (const perm of x.data ?? []) {
            if (!perm?.id || !perm.sessionID) continue
            const existing = grouped[perm.sessionID]
            if (existing) {
              existing.push(perm)
              continue
            }
            grouped[perm.sessionID] = [perm]
          }

          batch(() => {
            for (const sessionID of Object.keys(store.permission)) {
              if (grouped[sessionID]) continue
              setStore("permission", sessionID, [])
            }
            for (const [sessionID, permissions] of Object.entries(grouped)) {
              setStore(
                "permission",
                sessionID,
                reconcile(
                  permissions
                    .filter((p) => !!p?.id)
                    .slice()
                    .sort((a, b) => a.id.localeCompare(b.id)),
                  { key: "id" },
                ),
              )
            }
          })
        }),
        sdk.question.list().then((x) => {
          const grouped: Record<string, QuestionRequest[]> = {}
          for (const question of x.data ?? []) {
            if (!question?.id || !question.sessionID) continue
            const existing = grouped[question.sessionID]
            if (existing) {
              existing.push(question)
              continue
            }
            grouped[question.sessionID] = [question]
          }

          batch(() => {
            for (const sessionID of Object.keys(store.question)) {
              if (grouped[sessionID]) continue
              setStore("question", sessionID, [])
            }
            for (const [sessionID, questions] of Object.entries(grouped)) {
              setStore(
                "question",
                sessionID,
                reconcile(
                  questions
                    .filter((q) => !!q?.id)
                    .slice()
                    .sort((a, b) => a.id.localeCompare(b.id)),
                  { key: "id" },
                ),
              )
            }
          })
        }),
      ]).then(() => {
        setStore("status", "complete")
      })
    })()

    booting.set(directory, promise)
    promise.finally(() => {
      booting.delete(directory)
    })
    return promise
  }

  const unsub = globalSDK.event.listen((e) => {
    const directory = e.name
    const event = e.details

    if (directory === "global") {
      switch (event?.type) {
        case "global.disposed": {
          if (globalStore.reload) return
          bootstrap()
          break
        }
        case "project.updated": {
          const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id)
          if (result.found) {
            setGlobalStore("project", result.index, reconcile(event.properties))
            return
          }
          setGlobalStore(
            "project",
            produce((draft) => {
              draft.splice(result.index, 0, event.properties)
            }),
          )
          break
        }
      }
      return
    }

    const existing = children[directory]
    if (!existing) return

    const [store, setStore] = existing
    switch (event.type) {
      case "server.instance.disposed": {
        if (globalStore.reload) {
          bootstrapQueue.push(directory)
          return
        }
        bootstrapInstance(directory)
        break
      }
      case "session.created": {
        const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
        if (result.found) {
          setStore("session", result.index, reconcile(event.properties.info))
          break
        }
        setStore(
          "session",
          produce((draft) => {
            draft.splice(result.index, 0, event.properties.info)
          }),
        )
        if (!event.properties.info.parentID) {
          setStore("sessionTotal", store.sessionTotal + 1)
        }
        break
      }
      case "session.updated": {
        const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
        if (event.properties.info.time.archived) {
          if (result.found) {
            setStore(
              "session",
              produce((draft) => {
                draft.splice(result.index, 1)
              }),
            )
          }
          if (event.properties.info.parentID) break
          setStore("sessionTotal", (value) => Math.max(0, value - 1))
          break
        }
        if (result.found) {
          setStore("session", result.index, reconcile(event.properties.info))
          break
        }
        setStore(
          "session",
          produce((draft) => {
            draft.splice(result.index, 0, event.properties.info)
          }),
        )
        break
      }
      case "session.diff":
        setStore("session_diff", event.properties.sessionID, reconcile(event.properties.diff, { key: "file" }))
        break
      case "todo.updated":
        setStore("todo", event.properties.sessionID, reconcile(event.properties.todos, { key: "id" }))
        break
      case "session.status": {
        setStore("session_status", event.properties.sessionID, reconcile(event.properties.status))
        break
      }
      case "message.updated": {
        const messages = store.message[event.properties.info.sessionID]
        if (!messages) {
          setStore("message", event.properties.info.sessionID, [event.properties.info])
          break
        }
        const result = Binary.search(messages, event.properties.info.id, (m) => m.id)
        if (result.found) {
          setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info))
          break
        }
        setStore(
          "message",
          event.properties.info.sessionID,
          produce((draft) => {
            draft.splice(result.index, 0, event.properties.info)
          }),
        )
        break
      }
      case "message.removed": {
        const messages = store.message[event.properties.sessionID]
        if (!messages) break
        const result = Binary.search(messages, event.properties.messageID, (m) => m.id)
        if (result.found) {
          setStore(
            "message",
            event.properties.sessionID,
            produce((draft) => {
              draft.splice(result.index, 1)
            }),
          )
        }
        break
      }
      case "message.part.updated": {
        const part = event.properties.part
        const parts = store.part[part.messageID]
        if (!parts) {
          setStore("part", part.messageID, [part])
          break
        }
        const result = Binary.search(parts, part.id, (p) => p.id)
        if (result.found) {
          setStore("part", part.messageID, result.index, reconcile(part))
          break
        }
        setStore(
          "part",
          part.messageID,
          produce((draft) => {
            draft.splice(result.index, 0, part)
          }),
        )
        break
      }
      case "message.part.removed": {
        const parts = store.part[event.properties.messageID]
        if (!parts) break
        const result = Binary.search(parts, event.properties.partID, (p) => p.id)
        if (result.found) {
          setStore(
            "part",
            event.properties.messageID,
            produce((draft) => {
              draft.splice(result.index, 1)
            }),
          )
        }
        break
      }
      case "vcs.branch.updated": {
        const next = { branch: event.properties.branch }
        setStore("vcs", next)
        const cache = vcsCache.get(directory)
        if (cache) cache.setStore("value", next)
        break
      }
      case "permission.asked": {
        const sessionID = event.properties.sessionID
        const permissions = store.permission[sessionID]
        if (!permissions) {
          setStore("permission", sessionID, [event.properties])
          break
        }

        const result = Binary.search(permissions, event.properties.id, (p) => p.id)
        if (result.found) {
          setStore("permission", sessionID, result.index, reconcile(event.properties))
          break
        }

        setStore(
          "permission",
          sessionID,
          produce((draft) => {
            draft.splice(result.index, 0, event.properties)
          }),
        )
        break
      }
      case "permission.replied": {
        const permissions = store.permission[event.properties.sessionID]
        if (!permissions) break
        const result = Binary.search(permissions, event.properties.requestID, (p) => p.id)
        if (!result.found) break
        setStore(
          "permission",
          event.properties.sessionID,
          produce((draft) => {
            draft.splice(result.index, 1)
          }),
        )
        break
      }
      case "question.asked": {
        const sessionID = event.properties.sessionID
        const questions = store.question[sessionID]
        if (!questions) {
          setStore("question", sessionID, [event.properties])
          break
        }

        const result = Binary.search(questions, event.properties.id, (q) => q.id)
        if (result.found) {
          setStore("question", sessionID, result.index, reconcile(event.properties))
          break
        }

        setStore(
          "question",
          sessionID,
          produce((draft) => {
            draft.splice(result.index, 0, event.properties)
          }),
        )
        break
      }
      case "question.replied":
      case "question.rejected": {
        const questions = store.question[event.properties.sessionID]
        if (!questions) break
        const result = Binary.search(questions, event.properties.requestID, (q) => q.id)
        if (!result.found) break
        setStore(
          "question",
          event.properties.sessionID,
          produce((draft) => {
            draft.splice(result.index, 1)
          }),
        )
        break
      }
      case "lsp.updated": {
        const sdk = createOpencodeClient({
          baseUrl: globalSDK.url,
          fetch: platform.fetch,
          directory,
          throwOnError: true,
        })
        sdk.lsp.status().then((x) => setStore("lsp", x.data ?? []))
        break
      }
    }
  })
  onCleanup(unsub)

  async function bootstrap() {
    const health = await globalSDK.client.global
      .health()
      .then((x) => x.data)
      .catch(() => undefined)
    if (!health?.healthy) {
      setGlobalStore(
        "error",
        new Error(`Could not connect to server. Is there a server running at \`${globalSDK.url}\`?`),
      )
      return
    }

    return Promise.all([
      retry(() =>
        globalSDK.client.path.get().then((x) => {
          setGlobalStore("path", x.data!)
        }),
      ),
      retry(() =>
        globalSDK.client.config.get().then((x) => {
          setGlobalStore("config", x.data!)
        }),
      ),
      retry(() =>
        globalSDK.client.project.list().then(async (x) => {
          const projects = (x.data ?? [])
            .filter((p) => !!p?.id)
            .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
            .slice()
            .sort((a, b) => a.id.localeCompare(b.id))
          setGlobalStore("project", projects)
        }),
      ),
      retry(() =>
        globalSDK.client.provider.list().then((x) => {
          const data = x.data!
          setGlobalStore("provider", {
            ...data,
            all: data.all.map((provider) => ({
              ...provider,
              models: Object.fromEntries(
                Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated"),
              ),
            })),
          })
        }),
      ),
      retry(() =>
        globalSDK.client.provider.auth().then((x) => {
          setGlobalStore("provider_auth", x.data ?? {})
        }),
      ),
    ])
      .then(() => setGlobalStore("ready", true))
      .catch((e) => setGlobalStore("error", e))
  }

  onMount(() => {
    bootstrap()
  })

  return {
    data: globalStore,
    set: setGlobalStore,
    get ready() {
      return globalStore.ready
    },
    get error() {
      return globalStore.error
    },
    child,
    bootstrap,
    updateConfig: async (config: Config) => {
      setGlobalStore("reload", "pending")
      const response = await globalSDK.client.config.update({ config })
      setTimeout(() => {
        setGlobalStore("reload", "complete")
      }, 1000)
      return response
    },
    project: {
      loadSessions,
    },
  }
}

const GlobalSyncContext = createContext<ReturnType<typeof createGlobalSync>>()

export function GlobalSyncProvider(props: ParentProps) {
  const value = createGlobalSync()
  return (
    <Switch>
      <Match when={value.error}>
        <ErrorPage error={value.error} />
      </Match>
      <Match when={value.ready}>
        <GlobalSyncContext.Provider value={value}>{props.children}</GlobalSyncContext.Provider>
      </Match>
    </Switch>
  )
}

export function useGlobalSync() {
  const context = useContext(GlobalSyncContext)
  if (!context) throw new Error("useGlobalSync must be used within GlobalSyncProvider")
  return context
}
