import type { HookControlId, HookControlsPersistence, HookControlsResponse, HookFireStatsResponse, HookInventoryResponse, ItemsResponse, StateResponse } from './collector-types'
import type { ActivityQuery, ActivityReadResponse, ActivitySourceEntriesQuery, ActivitySourceEntriesResponse } from './activity-types'
import type { SessionScreenResponse } from './session-types'
import type { AttemptRecord } from '../components/plans/attempt-record'
import type { HarnessAttemptsResponse, HarnessDecisionsResponse, HarnessEffectivePlanResponse, HarnessEventsResponse, HarnessRunConfigResponse, HarnessRunDetailResponse } from './harness-types'
import type { FileIncidentRequest, FileIncidentResponse, Incident, IncidentOptionsResponse, IncidentsResponse, ListIncidentsParams } from './incident-types'
import type { ClusterNodeResponse, ClusterSnapshot } from './cluster-types'
import type { FactoryRunView } from './factory-types'
import type { RequestsResponse, RequestStoryV1 } from './request-types'
import type { ObservabilityReport, ObservabilityReportQuery } from '@overdeck/report-contract'
import { normalizeCollectorStateResponse } from './collector-state'

export class CollectorHttpError extends Error {
  constructor(readonly status: number, readonly body: unknown, message: string, readonly retryAfterMs: number | null = null) {
    super(message)
    this.name = 'CollectorHttpError'
  }
}

export const OBSERVABILITY_REPORT_TIMEOUT_MS = 120_000

export class ObservabilityReportTimeoutError extends Error {
  constructor(readonly timeoutMs: number) {
    super(`Observability report stopped after ${Math.round(timeoutMs / 1_000)} seconds without a response.`)
    this.name = 'ObservabilityReportTimeoutError'
  }
}

const REGISTERED_HOOK_CONTROL_IDS = ['background-jobs-blocker'] as const satisfies readonly HookControlId[]
const HOOK_CONTROLS_ISSUE_CODES = ['invalid-hook-controls', 'wrong-hook-controls-version'] as const
const HOOK_CONTROLS_PERSISTENCE = ['confirmed', 'indeterminate'] as const satisfies readonly HookControlsPersistence[]

function hookControlsClientError(payload: unknown, detail: string): CollectorHttpError {
  return new CollectorHttpError(200, payload, `collector hook controls response invalid: ${detail}`)
}

export function parseHookControlsResponse(payload: unknown): HookControlsResponse {
  if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) {
    throw hookControlsClientError(payload, 'expected an object with controls and issue')
  }
  const record = payload as Record<string, unknown>
  if (!('controls' in record) || !('issue' in record)) {
    throw hookControlsClientError(payload, 'missing required controls or issue')
  }
  const controls = record.controls
  if (controls === null || typeof controls !== 'object' || Array.isArray(controls)) {
    throw hookControlsClientError(payload, 'controls must be an object')
  }
  const controlsRecord = controls as Record<string, unknown>
  if (controlsRecord.version !== 'hook-controls/v1') {
    throw hookControlsClientError(payload, 'controls.version must be hook-controls/v1')
  }
  const hooks = controlsRecord.hooks
  if (hooks === null || typeof hooks !== 'object' || Array.isArray(hooks)) {
    throw hookControlsClientError(payload, 'controls.hooks must be an object')
  }
  const hooksRecord = hooks as Record<string, unknown>
  for (const id of REGISTERED_HOOK_CONTROL_IDS) {
    if (typeof hooksRecord[id] !== 'boolean') {
      throw hookControlsClientError(payload, `controls.hooks.${id} must be a boolean`)
    }
  }
  if (record.issue !== null) {
    if (typeof record.issue !== 'object' || Array.isArray(record.issue)) {
      throw hookControlsClientError(payload, 'issue must be null or an object')
    }
    const issue = record.issue as Record<string, unknown>
    if (!HOOK_CONTROLS_ISSUE_CODES.includes(issue.code as typeof HOOK_CONTROLS_ISSUE_CODES[number])) {
      throw hookControlsClientError(payload, 'issue.code is not a supported hook-controls issue code')
    }
    if (typeof issue.detail !== 'string') {
      throw hookControlsClientError(payload, 'issue.detail must be a string')
    }
    if (typeof issue.repairable !== 'boolean') {
      throw hookControlsClientError(payload, 'issue.repairable must be a boolean')
    }
  }
  const persistence = 'persistence' in record ? record.persistence : undefined
  const persistenceDetail = 'persistenceDetail' in record ? record.persistenceDetail : undefined
  if (persistence !== undefined) {
    if (!HOOK_CONTROLS_PERSISTENCE.includes(persistence as HookControlsPersistence)) {
      throw hookControlsClientError(payload, 'persistence must be confirmed or indeterminate')
    }
  }
  if (persistenceDetail !== undefined && typeof persistenceDetail !== 'string') {
    throw hookControlsClientError(payload, 'persistenceDetail must be a string')
  }
  const hasNonemptyDetail = typeof persistenceDetail === 'string' && persistenceDetail.length > 0
  const effectivePersistence = persistence ?? 'confirmed'
  if (hasNonemptyDetail && effectivePersistence !== 'indeterminate') {
    throw hookControlsClientError(payload, 'persistenceDetail requires persistence indeterminate')
  }
  if (effectivePersistence === 'indeterminate' && !hasNonemptyDetail) {
    throw hookControlsClientError(payload, 'indeterminate persistence requires non-empty persistenceDetail')
  }
  return payload as HookControlsResponse
}

// Same-origin only — src/pages/api/collector/[...path].ts proxies to the real collector
// server-side, so the browser never learns the collector's address or bearer token.

const MAX_RETRY_AFTER_MS = 5 * 60_000

function retryAfterMs(header: string | null): number | null {
  if (header === null) return null
  const parsed = /^\d+$/.test(header) ? Number(header) * 1_000 : Date.parse(header) - Date.now()
  return Number.isFinite(parsed) && parsed >= 0 ? Math.min(parsed, MAX_RETRY_AFTER_MS) : null
}

async function getJson<T>(url: string, init?: RequestInit): Promise<T> {
  const res = init === undefined ? await fetch(url) : await fetch(url, init)
  const body = await res.json().catch(() => null) as { error?: unknown; message?: unknown } | T | null
  if (!res.ok) {
    const detail = body && typeof body === 'object' && ('error' in body || 'message' in body)
      ? String(body.error ?? body.message)
      : `HTTP ${res.status}`
    throw new CollectorHttpError(res.status, body, `collector request failed: ${url} → ${detail}`, retryAfterMs(res.headers.get('retry-after')))
  }
  return body as T
}

async function postHookControlsJson(url: string, body: unknown): Promise<HookControlsResponse> {
  const res = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json', 'cache-control': 'no-cache' }, body: JSON.stringify(body), cache: 'no-store' })
  const payload = await res.json().catch(() => null) as HookControlsResponse | { detail?: unknown; message?: unknown; error?: unknown } | null
  if (!res.ok) {
    const fallback = `HTTP ${res.status}`
    let detail = fallback
    if (payload && typeof payload === 'object') {
      if ('detail' in payload && payload.detail != null) detail = String(payload.detail)
      else if ('message' in payload && payload.message != null) detail = String(payload.message)
      else if ('error' in payload && payload.error != null) detail = String(payload.error)
    }
    throw new CollectorHttpError(res.status, payload, `collector request failed: ${url} → ${detail}`)
  }
  return parseHookControlsResponse(payload)
}

async function postJson<T>(url: string, body: unknown): Promise<T> {
  const res = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
  const payload = await res.json().catch(() => null) as T | { error?: unknown } | null
  if (!res.ok) {
    const detail = payload && typeof payload === 'object' && 'error' in payload ? String(payload.error) : `HTTP ${res.status}`
    throw new CollectorHttpError(res.status, payload, `collector request failed: ${url} → ${detail}`)
  }
  return payload as T
}

export async function fetchCollectorState(): Promise<StateResponse> {
  return normalizeCollectorStateResponse(await getJson<unknown>('/api/collector/state'))
}

export function fetchFactoryRun(adwId: string, signal?: AbortSignal): Promise<FactoryRunView> {
  return getJson<FactoryRunView>(`/api/collector/factory/runs/${encodeURIComponent(adwId)}`, { signal })
}

export function fetchCluster(): Promise<ClusterSnapshot> {
  return getJson<ClusterSnapshot>('/api/collector/cluster')
}

export function fetchClusterNode(node: string): Promise<ClusterNodeResponse> {
  return getJson<ClusterNodeResponse>(`/api/collector/cluster/nodes/${encodeURIComponent(node)}`)
}

export function fetchCollectorItems(kind?: string): Promise<ItemsResponse> {
  const qs = kind ? `?kind=${encodeURIComponent(kind)}` : ''
  return getJson<ItemsResponse>(`/api/collector/items${qs}`)
}

export function fetchHookInventory(): Promise<HookInventoryResponse> {
  return getJson<HookInventoryResponse>('/api/collector/hooks')
}

export function fetchHookFireStats(): Promise<HookFireStatsResponse> {
  return getJson<HookFireStatsResponse>('/api/collector/hooks/fire-stats')
}

export async function fetchHookControls(options?: { signal?: AbortSignal }): Promise<HookControlsResponse> {
  return parseHookControlsResponse(await getJson<unknown>('/api/collector/config/hooks', {
    cache: 'no-store',
    signal: options?.signal,
  }))
}

export function setHookControl(id: HookControlId, enabled: boolean): Promise<HookControlsResponse> {
  return postHookControlsJson(`/api/collector/config/hooks/${encodeURIComponent(id)}`, { enabled })
}

export function repairHookControls(): Promise<HookControlsResponse> {
  return postHookControlsJson('/api/collector/config/hooks/repair', { confirm: 'replace-invalid-config' })
}

export async function fetchObservabilityReport(
  query: ObservabilityReportQuery,
  signal?: AbortSignal,
  timeoutMs = OBSERVABILITY_REPORT_TIMEOUT_MS,
): Promise<ObservabilityReport> {
  const params = new URLSearchParams({
    from: query.from,
    to: query.to,
    timezone: query.timezone,
  })
  query.projects?.forEach((project) => params.append('project', project))
  query.sources?.forEach((source) => params.append('source', source))
  if (query.severity) params.set('severity', query.severity)
  if (query.q) params.set('q', query.q)

  const controller = new AbortController()
  const abortFromCaller = () => controller.abort(signal?.reason)
  if (signal?.aborted) abortFromCaller()
  else signal?.addEventListener('abort', abortFromCaller, { once: true })
  const timeoutError = new ObservabilityReportTimeoutError(timeoutMs)
  const timeout = window.setTimeout(() => controller.abort(timeoutError), timeoutMs)
  try {
    return await getJson<ObservabilityReport>(`/api/collector/reports/observability?${params.toString()}`, {
      signal: controller.signal,
    })
  } catch (error) {
    if (controller.signal.reason === timeoutError) throw timeoutError
    throw error
  } finally {
    window.clearTimeout(timeout)
    signal?.removeEventListener('abort', abortFromCaller)
  }
}

export function fetchActivity(query?: ActivityQuery): Promise<ActivityReadResponse> {
  const params = new URLSearchParams()
  if (query?.from) params.set('from', query.from)
  if (query?.to) params.set('to', query.to)
  if (query?.category) params.set('category', query.category)
  if (query?.project) params.set('project', query.project)
  if (query?.severity) params.set('severity', query.severity)
  if (query?.limit !== undefined) params.set('limit', String(query.limit))
  if (query?.q) params.set('q', query.q)
  if (query?.node) params.set('host', query.node)
  if (query?.workload) params.set('workload', query.workload)
  if (query?.build) params.set('build', query.build)

  const queryString = params.toString()
  const path = queryString.length === 0 ? '/api/collector/activity' : `/api/collector/activity?${queryString}`
  return getJson<ActivityReadResponse>(path)
}

export function fetchActivitySourceEntries(query: ActivitySourceEntriesQuery): Promise<ActivitySourceEntriesResponse> {
  const params = new URLSearchParams()
  if (query.from) params.set('from', query.from)
  if (query.to) params.set('to', query.to)
  if (query.severity) params.set('severity', query.severity)
  if (query.q) params.set('q', query.q)
  if (query.recordId) params.set('recordId', query.recordId)
  if (query.offset !== undefined) params.set('offset', String(query.offset))
  if (query.limit !== undefined) params.set('limit', String(query.limit))

  const queryString = params.toString()
  const base = `/api/collector/activity/sources/${encodeURIComponent(query.sourceId)}/entries`
  return getJson<ActivitySourceEntriesResponse>(queryString.length === 0 ? base : `${base}?${queryString}`)
}

export interface DigestItem {
  title: string
  detail?: string
}

export interface DigestSection {
  id: 'runs' | 'scoreboard' | 'failures' | 'decisions'
  label: string
  items: DigestItem[]
}

export interface DigestResponse {
  day: string
  paragraph: string
  /** Absent from the collector's buildDigest response; only the paragraph is served today. */
  sections?: DigestSection[]
}

export function fetchCollectorDigest(): Promise<DigestResponse> {
  return getJson<DigestResponse>('/api/collector/digest')
}

export function collectorEventsUrl(): string {
  return '/api/collector/events'
}

export function fetchIncidents(params: ListIncidentsParams, signal?: AbortSignal): Promise<IncidentsResponse> {
  const search = new URLSearchParams({ scope: params.scope })
  if (params.query) search.set('query', params.query)
  if (params.priority) search.set('priority', params.priority)
  if (params.limit !== undefined) search.set('limit', String(params.limit))
  if (params.cursor !== undefined) search.set('cursor', String(params.cursor))
  if (params.highWater !== undefined) search.set('highWater', String(params.highWater))
  return getJson<IncidentsResponse>(`/api/collector/incidents?${search.toString()}`, { signal })
}

export function fetchRequests(signal?: AbortSignal): Promise<RequestsResponse> {
  return getJson<RequestsResponse>('/api/collector/requests', { signal })
}

export function fetchRequestStory(requestId: string, signal?: AbortSignal): Promise<{ story: RequestStoryV1 }> {
  return getJson<{ story: RequestStoryV1 }>(`/api/collector/requests/${encodeURIComponent(requestId)}/story`, { signal })
}

export function fetchIncident(incidentId: string): Promise<Incident> {
  return getJson<Incident>(`/api/collector/incidents/${encodeURIComponent(incidentId)}`)
}

export async function fileIncident(request: FileIncidentRequest): Promise<FileIncidentResponse> {
  const res = await fetch('/api/collector/incidents', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(request),
  })
  const body = await res.json().catch(() => null) as FileIncidentResponse | { error?: unknown } | null
  if (!res.ok) {
    const detail = body && typeof body === 'object' && 'error' in body ? String(body.error) : `HTTP ${res.status}`
    throw new CollectorHttpError(res.status, body, `incident filing failed: ${detail}`)
  }
  return body as FileIncidentResponse
}

export async function stopIncident(incidentId: string): Promise<Incident> {
  return (await postJson<{ incident: Incident }>(`/api/collector/incidents/${encodeURIComponent(incidentId)}/stop`, {})).incident
}

export async function deleteIncident(incidentId: string): Promise<void> {
  await postJson(`/api/collector/incidents/${encodeURIComponent(incidentId)}/delete`, {})
}

export async function dispatchIncident(incidentId: string, options: { withoutBrief?: boolean } = {}): Promise<Incident> {
  const body = options.withoutBrief === true ? { withoutBrief: true } : {}
  return (await postJson<{ incident: Incident }>(`/api/collector/incidents/${encodeURIComponent(incidentId)}/dispatch`, body)).incident
}

export function fetchIncidentOptions(): Promise<IncidentOptionsResponse> {
  return getJson<IncidentOptionsResponse>('/api/collector/incidents/options')
}

export function harnessTaskStreamUrl(runId: string, taskId: string): string {
  return `/api/collector/harness/runs/${encodeURIComponent(runId)}/tasks/${encodeURIComponent(taskId)}/stream`
}

const MAX_HARNESS_EVENT_PAGES = 64

export async function fetchHarnessEvents(runId: string, since?: string): Promise<HarnessEventsResponse> {
  const events: HarnessEventsResponse['events'] = []
  const seenEventIds = new Set<string>()
  const seenCursors = new Set(since === undefined ? [] : [since])
  let cursor = since

  for (let page = 0; page < MAX_HARNESS_EVENT_PAGES; page += 1) {
    const query = cursor === undefined ? '' : `?since=${encodeURIComponent(cursor)}`
    const response = await getJson<HarnessEventsResponse>(`/api/collector/harness/runs/${encodeURIComponent(runId)}/events${query}`)
    for (const event of response.events) {
      if (!seenEventIds.has(event.id)) {
        seenEventIds.add(event.id)
        events.push(event)
      }
    }
    if (!response.hasMore) return { ...response, events }
    if (seenCursors.has(response.nextSince)) throw new Error('collector event pagination repeated an opaque cursor')
    seenCursors.add(response.nextSince)
    cursor = response.nextSince
  }
  throw new Error(`collector event pagination exceeded ${MAX_HARNESS_EVENT_PAGES} pages`)
}

export function fetchHarnessConfig(runId: string): Promise<HarnessRunConfigResponse> {
  return getJson<HarnessRunConfigResponse>(`/api/collector/harness/runs/${encodeURIComponent(runId)}/config`)
}

export function fetchHarnessPlan(runId: string): Promise<HarnessEffectivePlanResponse> {
  return getJson<HarnessEffectivePlanResponse>(`/api/collector/harness/runs/${encodeURIComponent(runId)}/plan`)
}

export function fetchHarnessDecisions(runId: string): Promise<HarnessDecisionsResponse> {
  return getJson<HarnessDecisionsResponse>(`/api/collector/harness/runs/${encodeURIComponent(runId)}/decisions`)
}

export function fetchHarnessRunDetail(runId: string): Promise<HarnessRunDetailResponse> {
  return getJson<HarnessRunDetailResponse>(`/api/collector/harness/runs/${encodeURIComponent(runId)}`)
}

export async function fetchHarnessAttempts(runId: string): Promise<AttemptRecord[]> {
  const response = await getJson<HarnessAttemptsResponse>(`/api/collector/harness/runs/${encodeURIComponent(runId)}/attempts`)
  return response.attempts
}

export function fetchSessionScreen(sessionId: string): Promise<SessionScreenResponse> {
  return getJson<SessionScreenResponse>(`/api/collector/sessions/${encodeURIComponent(sessionId)}/screen`)
}

export interface HostLogsResponse {
  lines: string[]
}

export interface CollectorActionResult {
  ok: boolean
  result?: string
  error?: string
}

export async function fetchHostLogs(host: string): Promise<string[]> {
  const res = await fetch(`/api/collector/hosts/${encodeURIComponent(host)}/logs`)
  if (!res.ok) throw new Error(`host logs failed: ${host} → HTTP ${res.status}`)
  const data = (await res.json()) as HostLogsResponse
  return data.lines ?? []
}

export async function postCollectorAction(
  verb: string,
  args: Record<string, string>,
  requestedBy?: string,
): Promise<CollectorActionResult> {
  const res = await fetch(`/api/collector/actions/${encodeURIComponent(verb)}`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ args, requestedBy }),
  })
  const payload = await res.json().catch(() => ({})) as CollectorActionResult
  if (!res.ok) throw new CollectorHttpError(res.status, payload, payload.error || `collector action failed: ${verb} → HTTP ${res.status}`)
  return payload
}

export interface ProjectColorsConfig {
  projects: Record<string, string>
}

export function fetchProjectColors(): Promise<ProjectColorsConfig> {
  return getJson<ProjectColorsConfig>('/api/collector/config/projects')
}

export async function saveProjectColors(projects: Record<string, string>): Promise<void> {
  const res = await fetch('/api/collector/config/projects', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ projects }),
  })
  if (!res.ok) throw new Error(`save project colors failed: HTTP ${res.status}`)
}

export type RoutingProvider = 'codex' | 'claude'
export interface RoutingRules {
  version: 'routing/v2'
  projects: Record<string, string>
  default: string
  fallback_chain: string[]
  fallback_trigger: 'broken_only' | 'broken_or_quota_exhausted'
  missing_health_is_available: boolean
  quota_exhausted_threshold_pct: number
  account_caps: Record<string, Partial<Record<'5h' | '7d', number>>>
}
export interface RoutingConfigResponse {
  provider: RoutingProvider
  accounts: string[]
  rules: RoutingRules
}

export function fetchRoutingConfig(provider: RoutingProvider): Promise<RoutingConfigResponse> {
  return getJson<RoutingConfigResponse>(`/api/collector/config/routing/${provider}`)
}

export async function saveRoutingConfig(provider: RoutingProvider, rules: RoutingRules): Promise<void> {
  const res = await fetch(`/api/collector/config/routing/${provider}`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(rules),
  })
  const body = await res.json().catch(() => null) as { detail?: string } | null
  if (!res.ok) throw new Error(body?.detail ?? `save routing config failed: HTTP ${res.status}`)
}

/**
 * Maps a collector Panel id to its owning adapter id — `Panel` carries no adapter
 * field (collector/src/schema.ts), so this mirrors each adapter's own panel-emission
 * code (collector/src/adapters/{golive,systray,ghci,harness,prometheus}.ts).
 */
export function adapterIdForPanel(panelId: string): string | null {
  if (panelId === 'scoreboard') return 'golive'
  if (panelId === 'limits') return 'systray-ai'
  if (panelId === 'ci') return 'ghci'
  if (panelId === 'plans' || panelId.startsWith('forensics:')) return 'harness'
  if (panelId.startsWith('host:')) return 'prometheus'
  if (panelId === 'cluster' || panelId === 'agents') return 'cluster'
  if (panelId === 'gates') return 'gates'
  if (panelId === 'bots') return 'botmaster'
  if (panelId === 'factory-runs') return 'factory'
  if (panelId === 'sessions') return 'sessions'
  if (panelId === 'seats') return 'seats'
  return null
}
