import { TextAttributes } from "@opentui/core"
import type {
  ConnectionInfo,
  IntegrationCommandConnectOutput,
  IntegrationInfo,
  IntegrationOauthConnectOutput,
  IntegrationOAuthMethod,
} from "@opencode-ai/client"
import { createMemo, createSignal, onCleanup, onMount, Show } from "solid-js"
import { useClipboard } from "../context/clipboard"
import { useData } from "../context/data"
import { useClient } from "../context/client"
import { Keymap } from "../context/keymap"
import { useTheme } from "../context/theme"
import { useDialog } from "../ui/dialog"
import { DialogPrompt } from "../ui/dialog-prompt"
import { DialogSelect } from "../ui/dialog-select"
import { Link } from "../ui/link"
import { useToast } from "../ui/toast"

const INTEGRATION_PRIORITY: Record<string, number> = {
  opencode: 0,
  "opencode-go": 1,
  openai: 2,
  "github-copilot": 3,
  anthropic: 4,
  google: 5,
}

type ConnectMethod = Exclude<IntegrationInfo["methods"][number], { type: "env" }>
type IntegrationAttempt = IntegrationOauthConnectOutput["data"]
type CommandAttempt = IntegrationCommandConnectOutput["data"]
type OnIntegrationConnected = (providerID?: string) => void

export function integrationOptions(list: IntegrationInfo[]) {
  return list.toSorted(
    (a, b) =>
      (INTEGRATION_PRIORITY[a.id] ?? 99) - (INTEGRATION_PRIORITY[b.id] ?? 99) ||
      a.name.localeCompare(b.name) ||
      a.id.localeCompare(b.id),
  )
}

export function connectMethods(integration: IntegrationInfo): ConnectMethod[] {
  return integration.methods
    .filter((method): method is ConnectMethod => method.type !== "env")
    .toSorted((a, b) => Number(a.type === "key") - Number(b.type === "key"))
}

export function credentialConnections(integration: IntegrationInfo) {
  return integration.connections.filter(
    (connection): connection is Extract<ConnectionInfo, { type: "credential" }> => connection.type === "credential",
  )
}

export function connectionSummary(integration: IntegrationInfo) {
  return integration.connections
    .map((connection) => (connection.type === "credential" ? connection.label : `$${connection.name}`))
    .join(", ")
}

export function DialogIntegration(props: { onConnected?: OnIntegrationConnected } = {}) {
  const data = useData()
  const dialog = useDialog()
  const { themeV2 } = useTheme().contextual("elevated")
  const options = createMemo(() =>
    integrationOptions(data.location.integration.list() ?? []).map((integration) => {
      const methods = connectMethods(integration)
      const connected = integration.connections.length > 0
      return {
        title: integration.name,
        value: integration.id,
        description: methods.length ? undefined : "Environment only",
        footer: connectionSummary(integration) || undefined,
        category: integration.id in INTEGRATION_PRIORITY ? "Popular" : "Services",
        disabled: methods.length === 0,
        gutter: connected ? () => <text fg={themeV2.text.feedback.success.default}>✓</text> : undefined,
        onSelect: () =>
          credentialConnections(integration).length
            ? manageConnections(integration, methods, dialog, props.onConnected)
            : selectMethod(integration, methods, dialog, props.onConnected),
      }
    }),
  )

  return (
    <DialogSelect
      title="Connect a service"
      options={options()}
      emptyView={
        <box paddingLeft={4} paddingRight={4} paddingTop={1}>
          <text fg={themeV2.text.subdued}>No integrations available</text>
        </box>
      }
      noMatchView={
        <box paddingLeft={4} paddingRight={4} paddingTop={1}>
          <text fg={themeV2.text.subdued}>No integrations found</text>
        </box>
      }
    />
  )
}

function manageConnections(
  integration: IntegrationInfo,
  methods: ConnectMethod[],
  dialog: ReturnType<typeof useDialog>,
  onConnected?: OnIntegrationConnected,
) {
  dialog.replace(() => {
    const data = useData()
    const client = useClient()
    const toast = useToast()
    return (
      <DialogSelect
        title={integration.name}
        options={[
          ...(methods.length
            ? [
                {
                  title: "Add connection",
                  value: "add",
                  onSelect: () => selectMethod(integration, methods, dialog, onConnected),
                },
              ]
            : []),
          ...credentialConnections(integration).map((connection) => ({
            title: `Disconnect ${connection.label}`,
            value: connection.id,
            onSelect: () => {
              void client.api.credential
                .remove({ credentialID: connection.id, location: location(data) })
                .then(() => disconnected(integration.name, data, dialog, toast))
                .catch(toast.error)
            },
          })),
        ]}
      />
    )
  })
}

function selectMethod(
  integration: IntegrationInfo,
  methods: ConnectMethod[],
  dialog: ReturnType<typeof useDialog>,
  onConnected?: OnIntegrationConnected,
) {
  if (methods.length === 1) return openMethod(integration, methods[0], dialog, onConnected)
  dialog.replace(() => (
    <DialogSelect
      title={`Connect ${integration.name}`}
      options={methods.map((method) => ({
        title: method.type === "key" ? (method.label ?? "API key") : method.label,
        value: method.type === "key" ? "key" : method.id,
        onSelect: () => openMethod(integration, method, dialog, onConnected),
      }))}
    />
  ))
}

function openMethod(
  integration: IntegrationInfo,
  method: ConnectMethod,
  dialog: ReturnType<typeof useDialog>,
  onConnected?: OnIntegrationConnected,
) {
  if (method.type === "key") {
    dialog.replace(() => <KeyMethod integration={integration} method={method} onConnected={onConnected} />)
    return
  }
  if (method.type === "command") {
    dialog.replace(() => <CommandStarting integration={integration} method={method} onConnected={onConnected} />)
    return
  }
  void beginOAuth(integration, method, dialog, onConnected)
}

function CommandStarting(props: {
  integration: IntegrationInfo
  method: Extract<ConnectMethod, { type: "command" }>
  onConnected?: OnIntegrationConnected
}) {
  const data = useData()
  const dialog = useDialog()
  const client = useClient()
  const toast = useToast()
  let closed = false
  let handedOff = false

  onMount(() => {
    void client.api.integration.command
      .connect({
        integrationID: props.integration.id,
        methodID: props.method.id,
        location: location(data),
      })
      .then((result) => {
        if (closed) {
          void client.api.integration.command.cancel({
            integrationID: props.integration.id,
            attemptID: result.data.attemptID,
            location: location(data),
          })
          return
        }
        handedOff = true
        dialog.replace(() => (
          <CommandPending
            integration={props.integration}
            title={props.method.label}
            attempt={result.data}
            onConnected={props.onConnected}
          />
        ))
      })
      .catch((cause) => {
        if (closed) return
        toast.show({ variant: "error", message: message(cause) })
        dialog.clear()
      })
  })
  onCleanup(() => {
    if (!handedOff) closed = true
  })

  return <CommandView title={props.method.label} output="" message="Starting command..." />
}

function CommandPending(props: {
  integration: IntegrationInfo
  title: string
  attempt: CommandAttempt
  onConnected?: OnIntegrationConnected
}) {
  const data = useData()
  const dialog = useDialog()
  const client = useClient()
  const toast = useToast()
  const [output, setOutput] = createSignal("")
  let timer: ReturnType<typeof setTimeout> | undefined
  let settled = false

  const poll = () => {
    void client.api.integration.command
      .status({
        integrationID: props.integration.id,
        attemptID: props.attempt.attemptID,
        location: location(data),
      })
      .then((result) => {
        const status = result.data
        if (status.status === "pending") {
          setOutput(status.message ?? "")
          timer = setTimeout(poll, 500)
          return
        }
        settled = true
        if (status.status === "complete") {
          void connected(props.integration, data, dialog, toast, props.onConnected)
          return
        }
        toast.show({
          variant: "error",
          message: status.status === "failed" ? status.message : "Authentication expired",
        })
        dialog.clear()
      })
      .catch((cause) => {
        settled = true
        toast.show({ variant: "error", message: message(cause) })
        dialog.clear()
      })
  }

  onMount(poll)
  onCleanup(() => {
    if (timer) clearTimeout(timer)
    if (settled) return
    void client.api.integration.command.cancel({
      integrationID: props.integration.id,
      attemptID: props.attempt.attemptID,
      location: location(data),
    })
  })

  return <CommandView title={props.title} output={output()} message="Waiting for command to finish..." />
}

function CommandView(props: { title: string; output: string; message: string }) {
  const dialog = useDialog()
  const { themeV2 } = useTheme().contextual("elevated")
  const { themeV2: overlayTheme } = useTheme().contextual("overlay")
  onMount(() => dialog.setSize("large"))
  return (
    <box gap={1} paddingBottom={1}>
      <box flexDirection="row" justifyContent="space-between" paddingLeft={2} paddingRight={2}>
        <text attributes={TextAttributes.BOLD} fg={themeV2.text.default}>
          {props.title}
        </text>
        <text fg={themeV2.text.subdued} onMouseUp={() => dialog.clear()}>
          esc close
        </text>
      </box>
      <box
        backgroundColor={overlayTheme.background.default}
        paddingLeft={2}
        paddingRight={2}
        paddingTop={1}
        paddingBottom={1}
      >
        <text fg={overlayTheme.text.default}>{props.output.trim()}</text>
      </box>
      <box paddingLeft={2} paddingRight={2}>
        <text fg={themeV2.text.subdued}>{props.message}</text>
      </box>
    </box>
  )
}

function KeyMethod(props: {
  integration: IntegrationInfo
  method: Extract<ConnectMethod, { type: "key" }>
  onConnected?: OnIntegrationConnected
}) {
  const data = useData()
  const dialog = useDialog()
  const client = useClient()
  const toast = useToast()
  const { themeV2 } = useTheme().contextual("elevated")
  const [error, setError] = createSignal<string>()

  return (
    <DialogPrompt
      title={props.method.label ?? `Connect ${props.integration.name}`}
      placeholder="API key"
      onConfirm={(key) => {
        if (!key) return
        void client.api.integration.connect
          .key({
            integrationID: props.integration.id,
            location: location(data),
            key,
          })
          .then(() => connected(props.integration, data, dialog, toast, props.onConnected))
          .catch((cause) => setError(message(cause)))
      }}
      description={() => (
        <Show when={error()}>{(value) => <text fg={themeV2.text.feedback.error.default}>{value()}</text>}</Show>
      )}
    />
  )
}

async function beginOAuth(
  integration: IntegrationInfo,
  method: IntegrationOAuthMethod,
  dialog: ReturnType<typeof useDialog>,
  onConnected?: OnIntegrationConnected,
) {
  const inputs = method.prompts?.length ? await promptInputs(dialog, method.prompts) : {}
  if (inputs === null) return
  dialog.replace(() => (
    <OAuthStarting integration={integration} method={method} inputs={inputs} onConnected={onConnected} />
  ))
}

function OAuthStarting(props: {
  integration: IntegrationInfo
  method: IntegrationOAuthMethod
  inputs: Record<string, string>
  onConnected?: OnIntegrationConnected
}) {
  const data = useData()
  const dialog = useDialog()
  const client = useClient()
  const toast = useToast()

  onMount(() => {
    void client.api.integration.oauth
      .connect({
        integrationID: props.integration.id,
        location: location(data),
        methodID: props.method.id,
        inputs: props.inputs,
      })
      .then((result) => {
        if (result.data.mode === "code") {
          dialog.replace(() => (
            <OAuthCode
              integration={props.integration}
              title={props.method.label}
              attempt={result.data}
              onConnected={props.onConnected}
            />
          ))
          return
        }
        dialog.replace(() => (
          <OAuthAuto
            integration={props.integration}
            title={props.method.label}
            attempt={result.data}
            onConnected={props.onConnected}
          />
        ))
      })
      .catch((cause) => {
        toast.show({ variant: "error", message: message(cause) })
        dialog.clear()
      })
  })

  return <OAuthView title={props.method.label} message="Starting authorization..." />
}

function OAuthAuto(props: {
  integration: IntegrationInfo
  title: string
  attempt: IntegrationAttempt
  onConnected?: OnIntegrationConnected
}) {
  const data = useData()
  const dialog = useDialog()
  const client = useClient()
  const toast = useToast()
  const clipboard = useClipboard()
  let timer: ReturnType<typeof setTimeout> | undefined
  let settled = false

  Keymap.createLayer(() => ({
    mode: "modal",
    commands: [
      {
        bind: "c",
        title: "Copy authorization details",
        group: "Dialog",
        run: () => {
          const value = props.attempt.instructions.match(/[A-Z0-9]{4}-[A-Z0-9]{4,5}/)?.[0] ?? props.attempt.url
          clipboard
            .write?.(value)
            .then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
            .catch(toast.error)
        },
      },
    ],
  }))

  const poll = () => {
    void client.api.integration.oauth
      .status({ integrationID: props.integration.id, attemptID: props.attempt.attemptID, location: location(data) })
      .then((result) => {
        const status = result.data
        if (status.status === "pending") {
          timer = setTimeout(poll, 500)
          return
        }
        settled = true
        if (status.status === "complete") {
          void connected(props.integration, data, dialog, toast, props.onConnected)
          return
        }
        toast.show({ variant: "error", message: status.status === "failed" ? status.message : "Authorization expired" })
        dialog.clear()
      })
      .catch((cause) => {
        settled = true
        toast.show({ variant: "error", message: message(cause) })
        dialog.clear()
      })
  }

  onMount(poll)
  onCleanup(() => {
    if (timer) clearTimeout(timer)
    if (settled) return
    void client.api.integration.oauth.cancel({
      integrationID: props.integration.id,
      attemptID: props.attempt.attemptID,
      location: location(data),
    })
  })

  return (
    <OAuthView
      title={props.title}
      url={props.attempt.url}
      instructions={props.attempt.instructions}
      message="Waiting for authorization..."
      copy
    />
  )
}

function OAuthCode(props: {
  integration: IntegrationInfo
  title: string
  attempt: IntegrationAttempt
  onConnected?: OnIntegrationConnected
}) {
  const data = useData()
  const dialog = useDialog()
  const client = useClient()
  const toast = useToast()
  const { themeV2 } = useTheme().contextual("elevated")
  const [error, setError] = createSignal<string>()
  let settled = false

  onCleanup(() => {
    if (settled) return
    void client.api.integration.oauth.cancel({
      integrationID: props.integration.id,
      attemptID: props.attempt.attemptID,
      location: location(data),
    })
  })

  return (
    <DialogPrompt
      title={props.title}
      placeholder="Authorization code"
      onConfirm={(code) => {
        if (!code) return
        void client.api.integration.oauth
          .complete({
            integrationID: props.integration.id,
            attemptID: props.attempt.attemptID,
            location: location(data),
            code,
          })
          .then(() => {
            settled = true
            return connected(props.integration, data, dialog, toast, props.onConnected)
          })
          .catch((cause) => setError(message(cause)))
      }}
      description={() => (
        <box gap={1}>
          <text fg={themeV2.text.subdued}>{props.attempt.instructions}</text>
          <Link href={props.attempt.url} fg={themeV2.markdown.link} />
          <Show when={error()}>{(value) => <text fg={themeV2.text.feedback.error.default}>{value()}</text>}</Show>
        </box>
      )}
    />
  )
}

function OAuthView(props: { title: string; url?: string; instructions?: string; message: string; copy?: boolean }) {
  const dialog = useDialog()
  const { themeV2 } = useTheme().contextual("elevated")
  return (
    <box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
      <box flexDirection="row" justifyContent="space-between">
        <text attributes={TextAttributes.BOLD} fg={themeV2.text.default}>
          {props.title}
        </text>
        <text fg={themeV2.text.subdued} onMouseUp={() => dialog.clear()}>
          esc
        </text>
      </box>
      <Show when={props.url}>
        {(url) => (
          <box gap={1}>
            <Link href={url()} fg={themeV2.markdown.link} />
            <Show when={props.instructions}>
              {(instructions) => <text fg={themeV2.text.subdued}>{instructions()}</text>}
            </Show>
          </box>
        )}
      </Show>
      <text fg={themeV2.text.subdued}>{props.message}</text>
      <Show when={props.copy}>
        <text fg={themeV2.text.default}>
          c <span style={{ fg: themeV2.text.subdued }}>copy</span>
        </text>
      </Show>
    </box>
  )
}

async function promptInputs(
  dialog: ReturnType<typeof useDialog>,
  prompts: NonNullable<IntegrationOAuthMethod["prompts"]>,
) {
  const inputs: Record<string, string> = {}
  for (const prompt of prompts) {
    if (prompt.when) {
      const value = inputs[prompt.when.key]
      if (value === undefined) continue
      const matches = prompt.when.op === "eq" ? value === prompt.when.value : value !== prompt.when.value
      if (!matches) continue
    }
    if (prompt.type === "select") {
      const value = await new Promise<string | null>((resolve) => {
        dialog.replace(
          () => (
            <DialogSelect
              title={prompt.message}
              options={prompt.options.map((option) => ({
                title: option.label,
                value: option.value,
                description: option.hint,
              }))}
              onSelect={(option) => resolve(option.value)}
            />
          ),
          () => resolve(null),
        )
      })
      if (value === null) return null
      inputs[prompt.key] = value
      continue
    }
    const value = await new Promise<string | null>((resolve) => {
      dialog.replace(
        () => <DialogPrompt title={prompt.message} placeholder={prompt.placeholder} onConfirm={resolve} />,
        () => resolve(null),
      )
    })
    if (value === null) return null
    inputs[prompt.key] = value
  }
  return inputs
}

async function connected(
  integration: IntegrationInfo,
  data: ReturnType<typeof useData>,
  dialog: ReturnType<typeof useDialog>,
  toast: ReturnType<typeof useToast>,
  onConnected?: OnIntegrationConnected,
) {
  data.location.integration.invalidate()
  data.location.model.invalidate()
  data.location.provider.invalidate()
  await Promise.all([data.location.integration.sync(), data.location.model.sync(), data.location.provider.sync()])
  toast.show({ variant: "success", message: `Connected ${integration.name}` })
  if (onConnected) {
    onConnected(providerID(data, integration.id))
    return
  }
  dialog.clear()
}

function providerID(data: ReturnType<typeof useData>, integrationID: string) {
  const models = data.location.model.list() ?? []
  const matches = (data.location.provider.list() ?? []).filter(
    (provider) => provider.integrationID === integrationID || provider.id === integrationID,
  )
  return (
    matches.find((provider) =>
      models.some((model) => model.providerID === provider.id && model.status !== "deprecated"),
    )?.id ?? matches[0]?.id
  )
}

async function disconnected(
  name: string,
  data: ReturnType<typeof useData>,
  dialog: ReturnType<typeof useDialog>,
  toast: ReturnType<typeof useToast>,
) {
  data.location.integration.invalidate()
  data.location.model.invalidate()
  data.location.provider.invalidate()
  await Promise.all([data.location.integration.sync(), data.location.model.sync(), data.location.provider.sync()])
  toast.show({ variant: "success", message: `Disconnected ${name}` })
  dialog.clear()
}

function location(data: ReturnType<typeof useData>) {
  const current = data.location.default()
  return { directory: current.directory, workspace: current.workspaceID }
}

function message(cause: unknown) {
  if (cause instanceof Error) return cause.message
  return "Authentication failed"
}
