import { fetchWithAuth } from './config'
import type { ChatEvent, MessageContent } from './types'

export type { ChatEvent }

// Path prefixes only — NOT resolved URLs. fetchWithAuth resolves the actual
// origin (relative vs. configured apiUrl) fresh on every call via
// config.ts's apiBase(), so these can't be pre-resolved once at module load
// (the config may not be known yet at import time, e.g. before Config.svelte
// or a Wails-injected __OIKOS_CONFIG__ runs).
const BASE = '/agent'
const API = '/api/v1'

// A session IS a task: a goal-structured unit of work with a lifecycle status
// and an outcome. goal/outcome/summary/entity_id are empty until the agent sets
// them (see the task-board plan). status defaults to 'active'.
export interface Session {
  id: string
  title: string
  actor: string
  goal?: string
  status?: string // active | planning | executing | awaiting_input | done | failed | abandoned
  outcome?: string // success | failure | partial
  summary?: string
  entity_id?: string
  pending_approvals?: number
  created_at: string
  last_active_at: string
}

export interface Message {
  id: string
  session_id: string
  role: string
  content: MessageContent | string
  created_at: string
}

export async function fetchSessions(): Promise<Session[]> {
  const res = await fetchWithAuth(`${BASE}/sessions`)
  if (!res.ok) return []
  const data = await res.json()
  return data.sessions ?? []
}

export async function fetchMessages(sessionId: string): Promise<Message[]> {
  const res = await fetchWithAuth(`${BASE}/sessions/${sessionId}`)
  if (!res.ok) return []
  const data = await res.json()
  return data.messages ?? []
}

// null distinguishes "session doesn't exist" (404 — the session was deleted,
// or a persisted/deep-linked window id was never valid) from a transient
// fetch failure, which should keep returning []/retrying rather than
// permanently flip a window into a "not found" state. Only the initial
// per-window load (chat.ts's loadSessionChat) needs this distinction — the
// polling loops keep using fetchMessages, where swallowing a blip into []
// and trying again next tick is the right behavior.
export async function fetchMessagesOrNotFound(sessionId: string): Promise<Message[] | null> {
  const res = await fetchWithAuth(`${BASE}/sessions/${sessionId}`)
  if (res.status === 404) return null
  if (!res.ok) return []
  const data = await res.json()
  return data.messages ?? []
}

export async function deleteSession(sessionId: string): Promise<boolean> {
  const res = await fetchWithAuth(`${BASE}/sessions/${sessionId}`, { method: 'DELETE' })
  return res.ok
}

export async function resumeSession(sessionId: string): Promise<boolean> {
  const res = await fetchWithAuth(`${BASE}/sessions/${sessionId}/resume`, { method: 'POST' })
  return res.ok
}

export interface PlanStep {
  id: string
  seq: number
  title: string
  detail: string
  status: 'pending' | 'running' | 'done' | 'failed' | 'skipped' | 'blocked' | 'replaced'
  generation?: number
  execution_id?: string
  target_slug?: string
  started_at?: string
  finished_at?: string
}

export async function fetchPlan(sessionId: string): Promise<PlanStep[]> {
  const res = await fetchWithAuth(`${BASE}/sessions/${sessionId}/plan`)
  if (!res.ok) return []
  const data = await res.json()
  return data.steps ?? []
}

export interface SessionQuestion {
  id: string
  prompt: string
  context: { why?: string; options?: string[]; entities?: string[] }
  status: 'open' | 'answered' | 'dismissed'
  answer?: string
  created_at: string
  answered_at?: string
}

export async function fetchQuestions(sessionId: string): Promise<SessionQuestion[]> {
  const res = await fetchWithAuth(`${BASE}/sessions/${sessionId}/questions`)
  if (!res.ok) return []
  const data = await res.json()
  return data.questions ?? []
}

export async function answerQuestion(
  sessionId: string,
  questionId: string,
  answer: string
): Promise<boolean> {
  const res = await fetchWithAuth(`${BASE}/sessions/${sessionId}/questions/${questionId}/answer`, {
    method: 'POST',
    body: JSON.stringify({ answer })
  })
  return res.ok
}

export function streamChat(
  message: string,
  sessionId: string | null,
  onEvent: (ev: ChatEvent) => void,
  onError: (err: string) => void,
  onDone: () => void
): AbortController {
  const controller = new AbortController()

  fetchWithAuth(`${BASE}/chat`, {
    method: 'POST',
    body: JSON.stringify({ message, session_id: sessionId ?? undefined }),
    signal: controller.signal
  })
    .then(async (res) => {
      if (!res.ok) {
        onError(`HTTP ${res.status}`)
        return
      }
      const reader = res.body?.getReader()
      if (!reader) {
        onError('no response body')
        return
      }
      const decoder = new TextDecoder()
      let buffer = ''

      while (true) {
        const { done, value } = await reader.read()
        if (done) break
        buffer += decoder.decode(value, { stream: true })
        const lines = buffer.split('\n')
        buffer = lines.pop() ?? ''

        for (const line of lines) {
          if (line.startsWith('data: ')) {
            try {
              const ev: ChatEvent = JSON.parse(line.slice(6))
              onEvent(ev)
            } catch {
              // skip malformed
            }
          }
        }
      }
    })
    .catch((err) => {
      onError(err.message)
    })
    .finally(() => {
      onDone()
    })

  return controller
}

export interface DashboardSummary {
  entities_by_type: Record<string, number>
  entities_by_state: Record<string, number>
  health: { healthy: number; degraded: number; down: number; unknown: number }
  signals_by_severity: Record<string, number>
  approvals_pending: number
  executions_by_state: Record<string, number>
  event_rate: { bucket: string; count: number }[]
}

export async function fetchDashboardSummary(): Promise<DashboardSummary | null> {
  const res = await fetchWithAuth(`${API}/dashboard/summary`)
  if (!res.ok) return null
  return res.json()
}

export type EntityHealth = 'healthy' | 'degraded' | 'down' | 'unknown' | 'stale'

export interface Entity {
  id: string
  slug: string
  type: string
  name: string
  state?: string | null
  attributes: Record<string, unknown>
  version: number
  created_at: string
  updated_at: string
  health?: EntityHealth | null
  last_check_at?: string | null
  maintenance_until?: string | null
}

export interface EntityFilters {
  type?: string
  state?: string
  domain?: string
  layer?: string
  q?: string
}

export async function fetchEntities(filters: EntityFilters = {}): Promise<Entity[]> {
  const params = new URLSearchParams()
  if (filters.type) params.set('type', filters.type)
  if (filters.state) params.set('state', filters.state)
  if (filters.domain) params.set('domain', filters.domain)
  if (filters.layer) params.set('layer', filters.layer)
  if (filters.q) params.set('q', filters.q)
  params.set('limit', '200')
  const res = await fetchWithAuth(`${API}/entities?${params}`)
  if (!res.ok) return []
  const data = await res.json()
  return data.items ?? []
}

// The entities endpoint caps at 200/page — the Knowledge Base wants the
// whole set (it filters by type/search client-side now instead of scoping
// the fetch server-side), so page through via cursor until exhausted.
export async function fetchAllEntities(): Promise<Entity[]> {
  const all: Entity[] = []
  let cursor: string | undefined
  do {
    const params = new URLSearchParams({ limit: '200' })
    if (cursor) params.set('cursor', cursor)
    const res = await fetchWithAuth(`${API}/entities?${params}`)
    if (!res.ok) break
    const data = await res.json()
    all.push(...(data.items ?? []))
    cursor = data.next_cursor ?? undefined
  } while (cursor)
  return all
}

export type OntologyLayer = 'meta' | 'infrastructure' | 'governance' | 'cognition'

export interface EntityType {
  name: string
  parent_type?: string | null
  is_abstract: boolean
  domain: string
  layer: OntologyLayer
  description?: string | null
}

export type RelationshipCardinality = 'one-to-one' | 'one-to-many' | 'many-to-one' | 'many-to-many'

export interface RelationshipTypeDef {
  name: string
  inverse?: string | null
  source_type: string
  target_type: string
  cardinality: RelationshipCardinality
  description?: string | null
}

export interface Ontology {
  entityTypes: EntityType[]
  relationshipTypes: RelationshipTypeDef[]
}

export async function fetchOntology(): Promise<Ontology> {
  const res = await fetchWithAuth(`${API}/ontology`)
  if (!res.ok) return { entityTypes: [], relationshipTypes: [] }
  const data = await res.json()
  return { entityTypes: data.entity_types ?? [], relationshipTypes: data.relationship_types ?? [] }
}

// The graph endpoint has no layer param, so callers build a type→layer map from
// this to scope the graph client-side (the entities table filters server-side).
export async function fetchEntityTypes(): Promise<EntityType[]> {
  return (await fetchOntology()).entityTypes
}

export interface EventFilters {
  type?: string
  severity?: string
}

export async function fetchEvents(
  filters: EventFilters = {}
): Promise<import('./stores/events').OikosEvent[]> {
  const params = new URLSearchParams()
  if (filters.type) params.set('type', filters.type)
  if (filters.severity) params.set('severity', filters.severity)
  params.set('limit', '100')
  const res = await fetchWithAuth(`${API}/events?${params}`)
  if (!res.ok) return []
  const data = await res.json()
  return data.items ?? []
}

export interface Approval {
  id: string
  slug: string
  subject?: string | null
  action: string
  risk_class: string
  kind: 'execution' | 'policy-change' | 'pattern-activation'
  payload?: Record<string, unknown> | null
  status: 'pending' | 'approved' | 'denied' | 'expired' | 'revoked'
  expires_at: string
  decided_at?: string | null
  decided_by?: string | null
  created_at: string
}

export async function fetchApprovals(status?: string): Promise<Approval[]> {
  const params = new URLSearchParams()
  if (status) params.set('status', status)
  params.set('limit', '200')
  const res = await fetchWithAuth(`${API}/approvals?${params}`)
  if (!res.ok) return []
  const data = await res.json()
  return data.items ?? []
}

export async function decideApproval(
  id: string,
  decision: 'approve' | 'deny' | 'revoke',
  note?: string
): Promise<Approval | null> {
  const res = await fetchWithAuth(`${API}/approvals/${id}/decision`, {
    method: 'POST',
    body: JSON.stringify({ decision, note })
  })
  if (!res.ok) return null
  return res.json()
}

export interface Execution {
  id: string
  slug: string
  target?: string | null
  action: string
  risk_class: string
  status: string
  approval_id?: string | null
  agent_id?: string | null
  result?: Record<string, unknown> | null
  duration_ms?: number | null
  verified: boolean
  correlation_id: string
  started_at?: string | null
  completed_at?: string | null
  created_at: string
}

export async function fetchExecutions(status?: string): Promise<Execution[]> {
  const params = new URLSearchParams()
  if (status) params.set('status', status)
  params.set('limit', '200')
  const res = await fetchWithAuth(`${API}/executions?${params}`)
  if (!res.ok) return []
  const data = await res.json()
  return data.items ?? []
}

export async function getExecution(id: string): Promise<Execution | null> {
  const res = await fetchWithAuth(`${API}/executions/${id}`)
  if (!res.ok) return null
  return res.json()
}

export async function cancelExecution(id: string): Promise<Execution | null> {
  const res = await fetchWithAuth(`${API}/executions/${id}/cancel`, { method: 'POST' })
  if (!res.ok) return null
  return res.json()
}

export interface ActivityItem {
  id: string
  target: string
  verb: string
  summary: string
  risk_class: string
  status: string
  duration_ms: number | null
  error?: string
  created_at: string
  completed_at: string | null
}

export async function fetchRecentActivity(limit = 50): Promise<ActivityItem[]> {
  const res = await fetchWithAuth(`${API}/activity/recent?limit=${limit}`)
  if (!res.ok) return []
  const data = await res.json()
  return data.items ?? []
}

export interface CapabilityTimelineItem {
  verb: string
  first_success: string | null
  successes: number
  total: number
}

export async function fetchLearningTimeline(): Promise<CapabilityTimelineItem[]> {
  const res = await fetchWithAuth(`${API}/learning/timeline`)
  if (!res.ok) return []
  const data = await res.json()
  return data.items ?? []
}

export interface TrendBucket {
  day: string
  successes: number
  failures: number
}

export async function fetchLearningTrend(): Promise<TrendBucket[]> {
  const res = await fetchWithAuth(`${API}/learning/trend`)
  if (!res.ok) return []
  const data = await res.json()
  return data.items ?? []
}

export interface Pattern {
  id: string
  slug: string
  applies_type: string
  action: string
  pattern: string
  confidence: number
  evidence_count: number
  success_count?: number
  failure_count?: number
  status: string
  quarantined?: boolean
}

export async function fetchPatterns(): Promise<Pattern[]> {
  const res = await fetchWithAuth(`${API}/patterns`)
  if (!res.ok) return []
  const data = await res.json()
  return data.items ?? []
}

export interface Skill {
  id: string
  slug: string
  name: string
  applies_type?: string | null
  action: string
  status: string
  success_rate?: number | null
  last_used_at?: string | null
}

export async function fetchSkills(): Promise<Skill[]> {
  const res = await fetchWithAuth(`${API}/skills`)
  if (!res.ok) return []
  const data = await res.json()
  return data.items ?? []
}

export interface Signal {
  id: string
  slug: string
  kind: string
  severity: 'info' | 'warning' | 'critical'
  state: 'raised' | 'acknowledged' | 'acting' | 'muted' | 'resolved' | 'failed'
  target?: string | null
  evidence?: string | null
  likely_cause?: string | null
  occurrence_count: number
  flap_count: number
  hold_down_until?: string | null
  mute_until?: string | null
  first_seen_at: string
  last_seen_at: string
}

export async function fetchSignals(
  filters: { state?: string; severity?: string } = {}
): Promise<Signal[]> {
  const params = new URLSearchParams()
  if (filters.state) params.set('state', filters.state)
  if (filters.severity) params.set('severity', filters.severity)
  params.set('limit', '200')
  const res = await fetchWithAuth(`${API}/signals?${params}`)
  if (!res.ok) return []
  const data = await res.json()
  return data.items ?? []
}

export async function ackSignal(id: string): Promise<Signal | null> {
  const res = await fetchWithAuth(`${API}/signals/${id}/ack`, { method: 'POST' })
  if (!res.ok) return null
  return res.json()
}

export async function resolveSignal(id: string, note?: string): Promise<Signal | null> {
  const res = await fetchWithAuth(`${API}/signals/${id}/resolve`, {
    method: 'POST',
    body: JSON.stringify({ note })
  })
  if (!res.ok) return null
  return res.json()
}

export async function muteSignal(
  id: string,
  muteUntil: string,
  note?: string
): Promise<Signal | null> {
  const res = await fetchWithAuth(`${API}/signals/${id}/mute`, {
    method: 'POST',
    body: JSON.stringify({ mute_until: muteUntil, note })
  })
  if (!res.ok) return null
  return res.json()
}

export interface Relationship {
  source: string
  target: string
  type: string
  attributes?: Record<string, unknown> | null
  valid_from: string
  valid_to?: string | null
}

// Direct relationships of an entity, both directions — unlike fetchGraph's
// blast_radius (which only walks outgoing edges, so it can never surface an
// edge some other entity points at this one unless that entity is also
// reachable going forward from here), this hits a dedicated endpoint that
// matches on source_id OR target_id directly.
export async function fetchEntityRelations(id: string): Promise<Relationship[]> {
  const res = await fetchWithAuth(
    `${API}/entities/${encodeURIComponent(id)}/relations?direction=both`
  )
  if (!res.ok) return []
  const data = await res.json()
  return data.items ?? []
}

export type Health = 'healthy' | 'degraded' | 'down' | 'unknown'

export interface GraphView {
  nodes: Entity[]
  edges: Relationship[]
  truncated?: boolean
  health?: Record<string, Health>
}

export interface GraphFilters {
  root?: string
  depth?: number
  relType?: string[]
  includeStatus?: boolean
}

export async function fetchGraph(filters: GraphFilters = {}): Promise<GraphView | null> {
  const params = new URLSearchParams()
  if (filters.root) params.set('root', filters.root)
  if (filters.depth) params.set('depth', String(filters.depth))
  for (const rt of filters.relType ?? []) params.append('rel_type', rt)
  if (filters.includeStatus) params.append('include', 'status')
  const res = await fetchWithAuth(`${API}/graph?${params}`)
  if (!res.ok) return null
  return res.json()
}

export interface BlastRadiusItem {
  entity: Entity
  depth: number
}

// What is reachable from this entity by walking the dependency graph — the
// question .agents/OIKOS.md says the ontology exists to answer ("what breaks if
// strong goes down?"). Defined since early on but, until the entity window's
// Impact section, never called from anywhere in the app.
//
// Caveat the UI should surface rather than hide: blast_radius walks OUTGOING
// edges only, so it under-reports for an entity whose importance comes from
// things pointing AT it — lxc:caddy returns 4 entities despite terminating
// every *.hubris.network route.
export async function fetchBlastRadius(id: string, depth = 2): Promise<BlastRadiusItem[]> {
  const res = await fetchWithAuth(
    `${API}/entities/${encodeURIComponent(id)}/blast-radius?depth=${depth}`
  )
  if (!res.ok) return []
  const data = await res.json()
  return data.items ?? []
}

// id is commonly a slug, not a UUID (e.g. "document:infrastructure/network") —
// slugs can contain '/', which must be percent-encoded or it splits the path
// into extra segments the router won't match.
export async function fetchEntity(id: string): Promise<Entity | null> {
  const res = await fetchWithAuth(`${API}/entities/${encodeURIComponent(id)}`)
  if (!res.ok) return null
  return res.json()
}

export interface MetricSample {
  ts: string
  value?: number | null
  avg?: number | null
  min?: number | null
  max?: number | null
}

export interface MetricSeries {
  entity_id: string
  metric: string
  rollup: 'raw' | '1h' | '1d'
  samples: MetricSample[]
}

export async function fetchMetrics(entityId: string): Promise<MetricSeries[]> {
  const params = new URLSearchParams({ entity_id: entityId, rollup: 'auto' })
  const res = await fetchWithAuth(`${API}/metrics?${params}`)
  if (!res.ok) return []
  const data = await res.json()
  return data.items ?? []
}

export interface KnowledgeHit {
  id: string
  slug: string
  type: 'document' | 'runbook' | 'investigation'
  title: string
  snippet?: string
  linked_entities?: string[]
}

export interface KnowledgeItem {
  slug: string
  title: string
  kind: 'document' | 'runbook' | 'investigation'
  source: string
  tags: string[]
  updated_at: string
  agent_authored: boolean
}

export interface RecentKnowledge {
  stats: {
    total: number
    by_kind: Record<string, number>
    agent_authored: number
    last_7d: number
  }
  items: KnowledgeItem[]
}

export async function fetchRecentKnowledge(source?: string): Promise<RecentKnowledge> {
  const params = new URLSearchParams()
  if (source) params.set('source', source)
  const res = await fetchWithAuth(`${API}/knowledge/recent?${params}`)
  if (!res.ok) return { stats: { total: 0, by_kind: {}, agent_authored: 0, last_7d: 0 }, items: [] }
  return res.json()
}

export async function fetchEntityKnowledge(entityId: string): Promise<KnowledgeHit[]> {
  const res = await fetchWithAuth(`${API}/knowledge/${entityId}`)
  if (!res.ok) return []
  const data = await res.json()
  return data.items ?? []
}

export interface KnowledgeContent {
  title: string
  content: string
  source: string
  edited_by: string
  tags: string[]
  updated_at: string
  revisions: number
}

// Full markdown body for a document/investigation/runbook entity — distinct
// from fetchEntityKnowledge, which returns knowledge that references OTHER
// entities, not this entity's own content.
export async function fetchKnowledgeContent(id: string): Promise<KnowledgeContent | null> {
  const res = await fetchWithAuth(`${API}/knowledge/content/${encodeURIComponent(id)}`)
  if (!res.ok) return null
  return res.json()
}

// ─── Knowledge wiki: write path + drift tooling ────────────────────────────
//
// Everything below this line talks to internal/httpapi/knowledge_write.go
// and knowledge_drift.go — the operator-facing CRUD surface added alongside
// the wiki redesign. Before this, the only writer was the MCP tool the agent
// uses; the web UI could search and read but never create, correct, or
// retire a note.
//
// Mutations throw KnowledgeApiError on failure instead of returning null —
// unlike the read helpers above, a write failure usually has a specific,
// user-facing reason (409 "a note with this title already exists", 400
// "content cannot be empty") that the caller needs to display, not just a
// generic "something went wrong."

// Mirrors the RFC7807 problem+json shape internal/httpapi/problem.go writes.
export class KnowledgeApiError extends Error {
  status: number
  detail: string
  constructor(status: number, title: string, detail: string) {
    super(title)
    this.status = status
    this.detail = detail
  }
}

async function parseKnowledgeError(res: Response): Promise<never> {
  let title = `request failed (${res.status})`
  let detail = ''
  try {
    const body = await res.json()
    title = body.title ?? title
    detail = body.detail ?? ''
  } catch {
    // non-JSON error body — fall back to the generic title above
  }
  throw new KnowledgeApiError(res.status, title, detail)
}

export interface KnowledgeListItem {
  id: string
  slug: string
  title: string
  kind: 'document' | 'runbook' | 'investigation'
  source: string
  edited_by: string
  tags: string[]
  about: string[]
  size: number
  updated_at: string
  created_at: string
  revisions: number
}

// The full live set, body-free — backs the wiki navigator tree. Distinct
// from fetchRecentKnowledge, which caps at 200 and drives the stats/recency
// view; the tree needs every note plus linked-entity slugs for the
// group-by-entity arrangement.
// Throws KnowledgeApiError on failure rather than returning [] — an empty
// list here must mean "the collection really is empty," never "the request
// failed." Silently treating a 500/network error as [] previously left the
// whole wiki reporting "0 notes" indistinguishable from an actual outage;
// see Knowledge.svelte's loadItems for how the caller surfaces this.
export async function listKnowledge(): Promise<KnowledgeListItem[]> {
  const res = await fetchWithAuth(`${API}/knowledge/list`)
  if (!res.ok) return parseKnowledgeError(res)
  const data = await res.json()
  return data.items ?? []
}

export interface KnowledgeWriteInput {
  title?: string
  content?: string
  kind?: 'document' | 'investigation' | 'runbook'
  tags?: string[]
  folder?: string
  about?: string[]
}

export async function createKnowledge(
  input: KnowledgeWriteInput
): Promise<{ slug: string; id: string; linked: string[] }> {
  const res = await fetchWithAuth(`${API}/knowledge`, {
    method: 'POST',
    body: JSON.stringify(input)
  })
  if (!res.ok) return parseKnowledgeError(res)
  return res.json()
}

// idOrSlug identifies the note; only the fields present in `input` are
// changed (undefined = leave alone), matching the PUT handler's COALESCE
// semantics — see knowledge_write.go's serveUpdateKnowledge.
// `linked` echoes back which `about` slugs actually resolved (only present
// when `input.about` was supplied) — a typo'd entity slug otherwise fails
// server-side with nothing but a log line, so the caller can diff this
// against what it sent and warn about anything that silently didn't take.
export async function updateKnowledge(
  idOrSlug: string,
  input: KnowledgeWriteInput
): Promise<{ linked?: string[] }> {
  const res = await fetchWithAuth(`${API}/knowledge/content/${encodeURIComponent(idOrSlug)}`, {
    method: 'PUT',
    body: JSON.stringify(input)
  })
  if (!res.ok) return parseKnowledgeError(res)
  return res.json()
}

// Soft delete — the note moves to the trash (fetchKnowledgeTrash) and can be
// brought back with restoreKnowledge. Never a hard, unrecoverable delete.
export async function deleteKnowledge(idOrSlug: string): Promise<void> {
  const res = await fetchWithAuth(`${API}/knowledge/content/${encodeURIComponent(idOrSlug)}`, {
    method: 'DELETE'
  })
  if (!res.ok) return parseKnowledgeError(res)
}

export async function restoreKnowledge(idOrSlug: string): Promise<void> {
  const res = await fetchWithAuth(`${API}/knowledge/restore/${encodeURIComponent(idOrSlug)}`, {
    method: 'POST'
  })
  if (!res.ok) return parseKnowledgeError(res)
}

export interface KnowledgeTrashItem {
  slug: string
  title: string
  kind: string
  deleted_by: string
  deleted_at: string
}

// Throws on failure — see listKnowledge's comment on why "empty" and
// "failed" must not collapse into the same [].
export async function fetchKnowledgeTrash(): Promise<KnowledgeTrashItem[]> {
  const res = await fetchWithAuth(`${API}/knowledge/trash`)
  if (!res.ok) return parseKnowledgeError(res)
  const data = await res.json()
  return data.items ?? []
}

export interface KnowledgeRevision {
  id: number
  title: string
  content: string
  edited_by: string
  tags: string[]
  version_at: string
  revised_at: string
}

// Newest first. Works even for a soft-deleted note — inspecting what was
// lost is exactly when history matters most (see resolveKnowledgeEntityAny
// in knowledge_write.go).
export async function fetchKnowledgeRevisions(idOrSlug: string): Promise<KnowledgeRevision[]> {
  const res = await fetchWithAuth(`${API}/knowledge/revisions/${encodeURIComponent(idOrSlug)}`)
  if (!res.ok) return parseKnowledgeError(res)
  const data = await res.json()
  return data.items ?? []
}

export interface KnowledgeTag {
  tag: string
  uses: number
  variants: string[]
  // True when the same tag is stored under more than one casing (e.g.
  // "oom" / "OOM") — the tag manager badges these as needing a normalize.
  split: boolean
}

export async function fetchKnowledgeTags(): Promise<KnowledgeTag[]> {
  const res = await fetchWithAuth(`${API}/knowledge/tags`)
  if (!res.ok) return parseKnowledgeError(res)
  const data = await res.json()
  return data.items ?? []
}

// Rewrites every `from` tag to `to` across all live notes. Pass several
// `from` values to merge them into one; pass a tag's own case variants to
// normalize casing.
export async function renameKnowledgeTag(from: string[], to: string): Promise<number> {
  const res = await fetchWithAuth(`${API}/knowledge/tags/rename`, {
    method: 'POST',
    body: JSON.stringify({ from, to })
  })
  if (!res.ok) return parseKnowledgeError(res)
  const data = await res.json()
  return data.notes_updated ?? 0
}

export interface KnowledgeDuplicateMember {
  slug: string
  title: string
  kind: string
  size: number
  updated_at: string
  edited_by: string
}

export interface KnowledgeDuplicateCluster {
  members: KnowledgeDuplicateMember[]
  top_similarity: number
  total_size: number
}

// Title-similarity clusters — candidates for review, never a verdict. See
// the Go handler: notes that share a naming template (e.g. the five
// "Lifecycle: <verb> a node" runbooks) can cluster here despite being
// genuinely distinct documents, so the UI must let the operator inspect
// each cluster rather than offering a blind "merge all."
export async function fetchKnowledgeDuplicates(
  threshold?: number
): Promise<KnowledgeDuplicateCluster[]> {
  const params = threshold ? `?threshold=${threshold}` : ''
  const res = await fetchWithAuth(`${API}/knowledge/duplicates${params}`)
  if (!res.ok) return parseKnowledgeError(res)
  const data = await res.json()
  return data.clusters ?? []
}

export interface KnowledgeOrphan {
  slug: string
  title: string
  kind: string
  edited_by: string
  updated_at: string
  reasons: ('untagged' | 'unlinked' | 'stale')[]
}

export async function fetchKnowledgeOrphans(
  staleDays?: number
): Promise<{ items: KnowledgeOrphan[]; counts: Record<string, number> }> {
  const params = staleDays ? `?stale_days=${staleDays}` : ''
  const res = await fetchWithAuth(`${API}/knowledge/orphans${params}`)
  if (!res.ok) return parseKnowledgeError(res)
  return res.json()
}

// Folds `sources` into `target`: each source's body is appended under a
// provenance heading, tags are unioned, and the sources are soft-deleted
// (recoverable from trash, same as a plain delete).
export async function mergeKnowledge(
  target: string,
  sources: string[]
): Promise<{ merged: string[]; tags_added: string[] }> {
  const res = await fetchWithAuth(`${API}/knowledge/merge`, {
    method: 'POST',
    body: JSON.stringify({ target, sources })
  })
  if (!res.ok) return parseKnowledgeError(res)
  return res.json()
}

export async function fetchEntityEvents(
  entityId: string
): Promise<import('./stores/events').OikosEvent[]> {
  const params = new URLSearchParams({ entity_id: entityId, limit: '50' })
  const res = await fetchWithAuth(`${API}/events?${params}`)
  if (!res.ok) return []
  const data = await res.json()
  return data.items ?? []
}

export async function fetchEntitySignals(entityId: string): Promise<Signal[]> {
  const params = new URLSearchParams({ entity_id: entityId, limit: '50' })
  const res = await fetchWithAuth(`${API}/signals?${params}`)
  if (!res.ok) return []
  const data = await res.json()
  return data.items ?? []
}

export interface ExecutionLogChunk {
  seq: number
  stream: string
  chunk: string
  ts: string
}

// Streamed command output. Chunks land in execution_logs as the command runs,
// so this returns output for an execution that is still going — unlike
// `result.output`, which is only written once at the terminal state.
export async function fetchExecutionLogs(
  executionId: string
): Promise<{ items: ExecutionLogChunk[]; combined: string }> {
  const res = await fetchWithAuth(`${API}/executions/${executionId}/logs?limit=2000`)
  if (!res.ok) return { items: [], combined: '' }
  const data = await res.json()
  return { items: data.items ?? [], combined: data.combined ?? '' }
}

export async function fetchEntityExecutions(entityId: string): Promise<Execution[]> {
  const params = new URLSearchParams({ target: entityId, limit: '50' })
  const res = await fetchWithAuth(`${API}/executions?${params}`)
  if (!res.ok) return []
  const data = await res.json()
  return data.items ?? []
}

export interface EntityTask {
  task: Entity
  executionCount: number
}

// Executions aren't directly browsable from an entity in a useful way — what
// matters is which task/session acted on it, and how many times. There's no
// "incoming relationships" endpoint (blast_radius/graph only walks outgoing
// edges), so this composes it client-side: find executions targeting this
// entity, then check each task's own outgoing edges (task --involves--> entity
// directly, or task --involves--> execution) to attribute them. Cheap while
// the task count is small; would want a dedicated query if that changes.
export async function fetchEntityTasks(entity: Entity): Promise<EntityTask[]> {
  const [executions, tasks] = await Promise.all([
    fetchEntityExecutions(entity.id),
    fetchEntities({ type: 'task' })
  ])
  const executionIds = new Set(executions.map((e) => e.id))

  const results = await Promise.all(
    tasks.map(async (task): Promise<EntityTask | null> => {
      const g = await fetchGraph({ root: task.slug, depth: 1 })
      if (!g) return null
      const involvesThisEntity = g.edges.some(
        (e) => e.type === 'involves' && e.target === entity.slug
      )
      const nodeTypeById = new Map(g.nodes.map((n) => [n.id, n.type]))
      const idBySlug = new Map(g.nodes.map((n) => [n.slug, n.id]))
      const executionCount = g.edges.filter((e) => {
        if (e.type !== 'involves') return false
        const targetId = idBySlug.get(e.target)
        return (
          targetId != null &&
          nodeTypeById.get(targetId) === 'execution' &&
          executionIds.has(targetId)
        )
      }).length
      if (!involvesThisEntity && executionCount === 0) return null
      return { task, executionCount }
    })
  )
  return results.filter((r): r is EntityTask => r !== null)
}

export interface Check {
  id: string
  slug: string
  kind: 'http' | 'tcp' | 'disk' | 'cert-expiry' | 'drift' | 'ping' | 'ssh-script'
  target?: string | null
  target_type?: string | null
  config?: Record<string, unknown>
  interval_s: number
  timeout_s: number
  zone?: string | null
  enabled: boolean
  version: number
  // This check's own verdict. An entity's health is the worst of these across
  // its enabled checks, so this is what explains *why* an entity is degraded —
  // e.g. host:strong reads down because one ping probe fails while five
  // ssh-script checks pass. Null until the check has run at least once.
  last_health?: EntityHealth | null
  last_run_at?: string | null
}

export async function fetchChecksForTarget(targetSlug: string): Promise<Check[]> {
  const params = new URLSearchParams({ target: targetSlug, limit: '50' })
  const res = await fetchWithAuth(`${API}/checks?${params}`)
  if (!res.ok) return []
  const data = await res.json()
  return data.items ?? []
}

export async function patchCheck(
  id: string,
  version: number,
  patch: { enabled?: boolean; interval_s?: number; timeout_s?: number }
): Promise<Check | null> {
  const res = await fetchWithAuth(`${API}/checks/${id}`, {
    method: 'PATCH',
    headers: { 'If-Match': `"${version}"` },
    body: JSON.stringify(patch)
  })
  if (!res.ok) return null
  return res.json()
}

export interface AgentActivity {
  id: number
  ts: string
  agent_id: string
  session_id?: string | null
  activity_type: 'tool_call' | 'reasoning' | 'decision' | 'mcp_query' | 'escalation'
  tool_name?: string | null
  entity_id?: string | null
  input_summary?: string | null
  output_summary?: string | null
  duration_ms?: number | null
  token_count?: number | null
  success?: boolean | null
  correlation_id?: string | null
}

export async function fetchAgentActivity(
  filters: {
    agent_id?: string
    activity_type?: string
    entity_id?: string
    limit?: number
  } = {}
): Promise<AgentActivity[]> {
  const params = new URLSearchParams()
  if (filters.agent_id) params.set('agent_id', filters.agent_id)
  if (filters.activity_type) params.set('activity_type', filters.activity_type)
  if (filters.entity_id) params.set('entity_id', filters.entity_id)
  params.set('limit', String(filters.limit ?? 200))
  const res = await fetchWithAuth(`${API}/agent-activity?${params}`)
  if (!res.ok) return []
  const data = await res.json()
  return data.items ?? []
}

export async function searchKnowledge(q: string, limit = 50): Promise<KnowledgeHit[]> {
  const params = new URLSearchParams({ q, limit: String(limit) })
  const res = await fetchWithAuth(`${API}/knowledge/search?${params}`)
  if (!res.ok) return []
  const data = await res.json()
  return data.items ?? []
}

export interface AuditEntry {
  id: number
  ts: string
  actor_type: 'agent' | 'operator' | 'system' | 'scheduler'
  actor_id?: string | null
  action: string
  entity_id?: string | null
  method?: string | null
  path?: string | null
  status_code?: number | null
  detail?: Record<string, unknown>
  source_ip?: string | null
  correlation_id?: string | null
}

export async function fetchAudit(
  filters: {
    actor_type?: string
    actor_id?: string
    entity_id?: string
    action?: string
    correlation_id?: string
    limit?: number
  } = {}
): Promise<AuditEntry[]> {
  const params = new URLSearchParams()
  if (filters.actor_type) params.set('actor_type', filters.actor_type)
  if (filters.actor_id) params.set('actor_id', filters.actor_id)
  if (filters.entity_id) params.set('entity_id', filters.entity_id)
  if (filters.action) params.set('action', filters.action)
  if (filters.correlation_id) params.set('correlation_id', filters.correlation_id)
  params.set('limit', String(filters.limit ?? 200))
  const res = await fetchWithAuth(`${API}/audit?${params}`)
  if (!res.ok) return []
  const data = await res.json()
  return data.items ?? []
}
