import type { FastifyInstance, FastifyRequest } from 'fastify'
import { timingSafeEqual } from 'node:crypto'
import { z } from 'zod'
import { db } from '../db/index.js'

/**
 * Docker monitoring agents (self-hosted, push model). Agents on each Docker VM
 * POST a rich container report here; ArchNest stores the latest per host and
 * serves it read-only to the UI. See docs/docker-agent-monitoring.md.
 *
 * Ingest is gated by a shared ARCHNEST_AGENT_TOKEN (NOT the user JWT). Read
 * endpoints are behind the normal user authenticate hook.
 */

// Reports older than this (by server receive time) are flagged stale.
const STALE_AFTER_MS = Number(process.env.ARCHNEST_AGENT_STALE_MS) || 90_000

const HOST_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/

const portSchema = z.object({
  hostIp: z.string().optional(),
  hostPort: z.number().int().nullable().optional(),
  containerPort: z.number().int(),
  proto: z.string(),
})

const containerSchema = z.object({
  id: z.string().min(1).max(128),
  name: z.string().max(256),
  image: z.string().max(512).default(''),
  imageId: z.string().max(256).optional(),
  state: z.string().max(32).default('unknown'),
  status: z.string().max(256).default(''),
  createdAt: z.string().max(64).optional(),
  startedAt: z.string().max(64).optional(),
  restartCount: z.number().int().optional(),
  restartPolicy: z.string().max(64).optional(),
  health: z.string().max(32).optional(),
  ports: z.array(portSchema).max(200).default([]),
  networks: z.array(z.object({ name: z.string(), ip: z.string().optional() })).max(50).default([]),
  mounts: z
    .array(
      z.object({
        type: z.string().optional(),
        source: z.string().optional(),
        destination: z.string().optional(),
        rw: z.boolean().optional(),
      }),
    )
    .max(100)
    .default([]),
  env: z.array(z.object({ key: z.string(), value: z.string() })).max(500).default([]),
  command: z.string().max(2048).optional(),
  labels: z.record(z.string(), z.string()).optional(),
  stats: z
    .object({
      cpuPercent: z.number().optional(),
      memUsage: z.number().optional(),
      memLimit: z.number().optional(),
      netRxBytes: z.number().optional(),
      netTxBytes: z.number().optional(),
      blockReadBytes: z.number().optional(),
      blockWriteBytes: z.number().optional(),
    })
    .optional(),
})

const reportSchema = z.object({
  hostId: z.string().regex(HOST_ID_RE, 'Invalid hostId'),
  hostname: z.string().max(256).optional(),
  agentVersion: z.string().max(32).optional(),
  reportedAt: z.string().max(64).optional(),
  containers: z.array(containerSchema).max(1000),
})

export type AgentReport = z.infer<typeof reportSchema>
export type AgentContainer = z.infer<typeof containerSchema>

interface ReportRow {
  host_id: string
  hostname: string | null
  report_json: string
  reported_at: string | null
  received_at: string
}

/** Constant-time bearer-token check against ARCHNEST_AGENT_TOKEN. */
function agentTokenValid(req: FastifyRequest): { ok: boolean; configured: boolean } {
  const expected = process.env.ARCHNEST_AGENT_TOKEN
  if (!expected) return { ok: false, configured: false }
  const header = req.headers.authorization ?? ''
  const presented = header.startsWith('Bearer ') ? header.slice(7) : ''
  const a = Buffer.from(presented)
  const b = Buffer.from(expected)
  if (a.length !== b.length) return { ok: false, configured: true }
  return { ok: timingSafeEqual(a, b), configured: true }
}

function isStale(receivedAt: string): boolean {
  const t = Date.parse(receivedAt.replace(' ', 'T') + 'Z')
  if (Number.isNaN(t)) return false
  return Date.now() - t > STALE_AFTER_MS
}

/** Token-gated ingest. Registered separately so it is NOT behind the user-auth hook. */
export async function agentIngestRoutes(app: FastifyInstance) {
  app.post('/api/agents/docker/report', async (req, reply) => {
    const auth = agentTokenValid(req)
    if (!auth.configured) {
      return reply.code(503).send({ error: 'Agent ingest is disabled (ARCHNEST_AGENT_TOKEN not configured)' })
    }
    if (!auth.ok) {
      return reply.code(401).send({ error: 'Unauthorized' })
    }
    const parsed = reportSchema.safeParse(req.body)
    if (!parsed.success) {
      return reply.code(400).send({ error: parsed.error.issues[0]?.message ?? 'Invalid report' })
    }
    const { hostId, hostname, reportedAt, containers } = parsed.data
    db.prepare(
      `INSERT INTO docker_agent_reports (host_id, hostname, report_json, reported_at, received_at)
       VALUES (?, ?, ?, ?, datetime('now'))
       ON CONFLICT(host_id) DO UPDATE SET
         hostname = excluded.hostname,
         report_json = excluded.report_json,
         reported_at = excluded.reported_at,
         received_at = datetime('now')`,
    ).run(hostId, hostname ?? null, JSON.stringify(containers), reportedAt ?? null)
    return { ok: true }
  })
}

/** Read-only query endpoints, behind the user authenticate hook. */
export async function agentRoutes(app: FastifyInstance) {
  app.addHook('onRequest', app.authenticate)

  app.get('/api/agents/docker/hosts', async () => {
    const rows = db
      .prepare('SELECT host_id, hostname, report_json, reported_at, received_at FROM docker_agent_reports ORDER BY host_id')
      .all() as ReportRow[]
    return {
      hosts: rows.map((r) => {
        let count = 0
        try {
          count = (JSON.parse(r.report_json) as unknown[]).length
        } catch {
          count = 0
        }
        return {
          hostId: r.host_id,
          hostname: r.hostname,
          reportedAt: r.reported_at,
          receivedAt: r.received_at,
          containerCount: count,
          stale: isStale(r.received_at),
        }
      }),
    }
  })

  app.get('/api/agents/docker/hosts/:hostId/containers', async (req, reply) => {
    const hostId = (req.params as { hostId: string }).hostId
    const row = db
      .prepare('SELECT host_id, hostname, report_json, reported_at, received_at FROM docker_agent_reports WHERE host_id = ?')
      .get(hostId) as ReportRow | undefined
    if (!row) return reply.code(404).send({ error: 'Host not reported' })
    let containers: AgentContainer[] = []
    try {
      containers = JSON.parse(row.report_json) as AgentContainer[]
    } catch {
      containers = []
    }
    return {
      hostId: row.host_id,
      hostname: row.hostname,
      reportedAt: row.reported_at,
      receivedAt: row.received_at,
      stale: isStale(row.received_at),
      containers,
    }
  })

  app.get('/api/agents/docker/hosts/:hostId/containers/:containerId', async (req, reply) => {
    const { hostId, containerId } = req.params as { hostId: string; containerId: string }
    const row = db
      .prepare('SELECT report_json FROM docker_agent_reports WHERE host_id = ?')
      .get(hostId) as { report_json: string } | undefined
    if (!row) return reply.code(404).send({ error: 'Host not reported' })
    let containers: AgentContainer[] = []
    try {
      containers = JSON.parse(row.report_json) as AgentContainer[]
    } catch {
      containers = []
    }
    // Match by full id or a unique short-id prefix.
    const container = containers.find((c) => c.id === containerId || c.id.startsWith(containerId))
    if (!container) return reply.code(404).send({ error: 'Container not found in latest report' })
    return { container }
  })
}
