/**
 * Control-plane ADMIN API client. Each method takes a freshly-minted `manage` bearer token and calls
 * the loopback CP admin surface. Responses are Zod-validated at the boundary (the CP is trusted for
 * auth but its payloads are still external data → validate, never `as`). Non-2xx ⇒ `CpClientError`.
 *
 * CP contract (control-plane/src/api/provision.ts, mounted at root, `Authorization: Bearer <token>`):
 *   - GET    /accounts/:id/hosts       → 200 [HostRecord...]        (agentPubkey base64)
 *   - POST   /accounts/:id/pairing-codes → 201 { code, expiresAt }
 *   - DELETE /hosts/:hostId            → 204
 *
 * We expose only a curated host view to the SPA (never leak agentPubkey/enrollFpr).
 */
import { z } from 'zod'

export interface HostView {
  readonly hostId: string
  readonly subdomain: string
  readonly status: string
  readonly lastSeen: string | undefined
  readonly createdAt: string | undefined
  readonly revokedAt: string | null | undefined
  /** Cert expiry, if the CP ever includes one (not in today's HostRecord) — surfaced when present. */
  readonly notAfter: string | undefined
}

export interface IssuedPairing {
  readonly code: string
  readonly expiresAt: string
}

export interface CpClient {
  listHosts(accountId: string, token: string): Promise<readonly HostView[]>
  createPairingCode(accountId: string, token: string): Promise<IssuedPairing>
  deleteHost(hostId: string, token: string): Promise<void>
}

/** Upstream failure — carries an HTTP-ish status for the route layer to map (kept generic; no CP body leaked). */
export class CpClientError extends Error {
  readonly status: number
  constructor(status: number, message: string) {
    super(message)
    this.name = 'CpClientError'
    this.status = status
  }
}

// Lenient: require the fields we render; passthrough-tolerate everything else the CP adds/removes.
const HostRecordSchema = z
  .object({
    hostId: z.string(),
    subdomain: z.string(),
    status: z.string(),
    lastSeen: z.string().optional(),
    createdAt: z.string().optional(),
    revokedAt: z.string().nullable().optional(),
    notAfter: z.string().optional(),
  })
  .passthrough()

const HostListSchema = z.array(HostRecordSchema)
const IssuedPairingSchema = z.object({ code: z.string().min(1), expiresAt: z.string().min(1) })

function toHostView(rec: z.infer<typeof HostRecordSchema>): HostView {
  return {
    hostId: rec.hostId,
    subdomain: rec.subdomain,
    status: rec.status,
    lastSeen: rec.lastSeen,
    createdAt: rec.createdAt,
    revokedAt: rec.revokedAt,
    notAfter: rec.notAfter,
  }
}

type FetchFn = typeof fetch

interface CpClientDeps {
  readonly cpUrl: string
  readonly fetchFn?: FetchFn
}

async function readJson(res: Response): Promise<unknown> {
  try {
    return await res.json()
  } catch {
    throw new CpClientError(502, 'control-plane returned a non-JSON response')
  }
}

export function createCpClient(deps: CpClientDeps): CpClient {
  const doFetch: FetchFn = deps.fetchFn ?? fetch
  const authHeaders = (token: string): Record<string, string> => ({ authorization: `Bearer ${token}`, accept: 'application/json' })

  const call = async (path: string, init: RequestInit): Promise<Response> => {
    try {
      return await doFetch(`${deps.cpUrl}${path}`, init)
    } catch {
      // Network/DNS/connection failure — the CP is unreachable.
      throw new CpClientError(502, 'control-plane unreachable')
    }
  }

  return {
    async listHosts(accountId, token) {
      const res = await call(`/accounts/${encodeURIComponent(accountId)}/hosts`, { headers: authHeaders(token) })
      if (!res.ok) throw new CpClientError(res.status, `list hosts failed (${res.status})`)
      const parsed = HostListSchema.parse(await readJson(res))
      return parsed.map(toHostView)
    },

    async createPairingCode(accountId, token) {
      const res = await call(`/accounts/${encodeURIComponent(accountId)}/pairing-codes`, {
        method: 'POST',
        headers: { ...authHeaders(token), 'content-type': 'application/json' },
        body: '{}',
      })
      if (!res.ok) throw new CpClientError(res.status, `issue pairing code failed (${res.status})`)
      const parsed = IssuedPairingSchema.parse(await readJson(res))
      return { code: parsed.code, expiresAt: parsed.expiresAt }
    },

    async deleteHost(hostId, token) {
      const res = await call(`/hosts/${encodeURIComponent(hostId)}`, { method: 'DELETE', headers: authHeaders(token) })
      if (!res.ok && res.status !== 204) throw new CpClientError(res.status, `revoke host failed (${res.status})`)
    },
  }
}
