/**
 * T2 — Zod-validated control-plane HTTP client.
 *
 * INV3 (owned): the request builders have NO parameter for account/tenant; every call carries only
 * the same-origin cookie/passkey session (`credentials: 'include'`). The account is derived
 * server-side. Every response is parsed through a Zod schema before return; a parse failure throws
 * a typed {@link ApiError} rather than returning a torn object.
 */
import { z } from 'zod'
import type { RelayWebConfig } from './config'
import { ApiError } from './errors'
import {
  CapabilityTokenResponseSchema,
  HostListWireSchema,
  HostRecordWireSchema,
  HostStatusResponseSchema,
  PairingCodeResponseSchema,
  type CapabilityRight,
  type HostRecord,
  type HostStatus,
} from './api-schemas'

/** v0.8 surface — ships against the flat SQLite table + cookie session. No token issuance. */
export interface ApiClientV08 {
  listHosts(): Promise<readonly HostRecord[]>
  getHost(hostId: string): Promise<HostRecord>
  requestPairingCode(): Promise<{ readonly code: string; readonly expiresAt: string }>
  hostStatus(hostId: string): Promise<HostStatus>
}

/** v0.9+ surface — GROWS once P5 capability-token issuance exists (INDEX §1). */
export interface ApiClient extends ApiClientV08 {
  issueCapabilityToken(hostId: string, rights: readonly CapabilityRight[]): Promise<string>
}

const JSON_HEADERS: Readonly<Record<string, string>> = { Accept: 'application/json' }

/** Map a non-OK HTTP status to a typed ApiError kind (no silent swallow). */
function errorForStatus(status: number, path: string): ApiError {
  if (status === 401) return new ApiError('unauthenticated', `unauthenticated at ${path}`, status)
  if (status === 403) return new ApiError('forbidden', `forbidden at ${path}`, status)
  return new ApiError('http', `request to ${path} failed with status ${status}`, status)
}

export function createApiClient(cfg: RelayWebConfig, fetchImpl: typeof fetch = fetch): ApiClient {
  const base = cfg.apiBase

  async function requestJson(path: string, init?: RequestInit): Promise<unknown> {
    let res: Response
    try {
      res = await fetchImpl(`${base}${path}`, {
        credentials: 'include', // cookie/passkey session — the ONLY identity (INV3)
        headers: JSON_HEADERS,
        ...init,
      })
    } catch (cause) {
      throw new ApiError('network', `network error calling ${path}`, null)
    }
    if (!res.ok) throw errorForStatus(res.status, path)
    try {
      return await res.json()
    } catch {
      throw new ApiError('parse', `invalid JSON body from ${path}`, res.status)
    }
  }

  /** Parse `body` with `schema`, converting a ZodError into a typed ApiError('parse'). */
  function validate<S extends z.ZodTypeAny>(schema: S, body: unknown, path: string): z.infer<S> {
    const parsed = schema.safeParse(body)
    if (!parsed.success) {
      throw new ApiError('parse', `malformed response from ${path}: ${parsed.error.message}`)
    }
    return parsed.data
  }

  function postJson(path: string, payload: unknown): Promise<unknown> {
    return requestJson(path, {
      method: 'POST',
      headers: { ...JSON_HEADERS, 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    })
  }

  return {
    async listHosts(): Promise<readonly HostRecord[]> {
      const path = '/api/hosts'
      return validate(HostListWireSchema, await requestJson(path), path)
    },

    async getHost(hostId: string): Promise<HostRecord> {
      const path = `/api/hosts/${encodeURIComponent(hostId)}`
      return validate(HostRecordWireSchema, await requestJson(path), path)
    },

    async requestPairingCode(): Promise<{ readonly code: string; readonly expiresAt: string }> {
      const path = '/api/pairing-codes'
      // Body is empty: the account is derived from the session, NEVER sent (INV3).
      return validate(PairingCodeResponseSchema, await postJson(path, {}), path)
    },

    async hostStatus(hostId: string): Promise<HostStatus> {
      const path = `/api/hosts/${encodeURIComponent(hostId)}/status`
      const parsed = validate(HostStatusResponseSchema, await requestJson(path), path)
      return parsed.status
    },

    async issueCapabilityToken(
      hostId: string,
      rights: readonly CapabilityRight[],
    ): Promise<string> {
      const path = `/api/hosts/${encodeURIComponent(hostId)}/capability-token`
      // Only the rights subset is sent; host+account scope is bound server-side from the session.
      const parsed = validate(CapabilityTokenResponseSchema, await postJson(path, { rights }), path)
      return parsed.token
    },
  }
}
