import { readFileSync } from 'node:fs';
import { randomUUID } from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import type { Sql } from '../db.js';
import type { Config } from '../config.js';
import { resolveProviderBaseUrl } from '../services/llama-providers.js';
import type { AppContext } from '../app-context.js';
import {
  validateLlamaConfig,
  computeDiff,
  readRemoteConfig,
  applyRemoteConfig,
  sshExec,
  type SshTarget,
  type SshExec,
  type SshMode,
} from '../services/ssh-config.js';
import { runModelPull, validateRepoId } from '../services/model-pull.js';

/**
 * P9.1: SSH config editor for llama-swap hosts.
 *
 * GET   /api/hosts                       — list control_hosts with SSH config status
 * PATCH /api/hosts/:id                    — set ssh_host/ssh_user/ssh_key_path/config_path/restart_cmd
 * GET   /api/hosts/:id/config             — SSH read the remote config
 * POST  /api/hosts/:id/config/validate    — validate a candidate config (no host touch)
 * POST  /api/hosts/:id/config/diff        — diff a candidate vs the live remote config
 * POST  /api/hosts/:id/config/apply       — validate -> backup -> write -> restart -> health-wait
 * POST  /api/hosts/:id/pull               — pull a HuggingFace model (non-blocking job)
 *
 * `exec` is injectable for tests; production uses the real `sshExec` (spawn ssh).
 */
export function registerSshConfigRoutes(
  app: FastifyInstance,
  ctx: AppContext,
  exec: SshExec = sshExec,
): void {
  const { sql, config, fleet, emitter } = ctx;
  const schema = loadConfigSchema(config);

  app.get('/api/hosts', async (_req: FastifyRequest, reply: FastifyReply) => {
    const rows = await sql<HostRow[]>`
      SELECT provider_id, ssh_host, ssh_user, ssh_key_path, config_path, restart_cmd, ssh_mode, os, gpu_label, enabled
      FROM control_hosts ORDER BY provider_id
    `;
    return reply.send({
      hosts: rows.map((r) => ({
        providerId: r.provider_id,
        sshHost: r.ssh_host,
        sshUser: r.ssh_user,
        sshKeyPath: r.ssh_key_path,
        configPath: r.config_path,
        restartCmd: r.restart_cmd,
        sshMode: r.ssh_mode ?? 'shell',
        os: r.os,
        gpuLabel: r.gpu_label,
        enabled: r.enabled,
        sshConfigured: !!(r.ssh_host && r.ssh_user && r.ssh_key_path && r.config_path),
      })),
    });
  });

  app.patch('/api/hosts/:id', async (req: FastifyRequest, reply: FastifyReply) => {
    const { id } = req.params as { id: string };
    const body = (req.body as Record<string, unknown>) ?? {};
    const sshHost = (body.sshHost as string) ?? null;
    const sshUser = (body.sshUser as string) ?? null;
    const sshKeyPath = (body.sshKeyPath as string) ?? null;
    const configPath = (body.configPath as string) ?? null;
    const restartCmd = (body.restartCmd as string) ?? null;
    const sshMode: SshMode = body.sshMode === 'wrapper' ? 'wrapper' : 'shell';

    const rows = await sql`
      UPDATE control_hosts
      SET ssh_host = ${sshHost}, ssh_user = ${sshUser}, ssh_key_path = ${sshKeyPath},
          config_path = ${configPath}, restart_cmd = ${restartCmd}, ssh_mode = ${sshMode}
      WHERE provider_id = ${id}
      RETURNING provider_id
    `;
    if (rows.length === 0) {
      return reply.status(404).send({ error: 'host not found' });
    }
    return reply.send({ ok: true });
  });

  app.get('/api/hosts/:id/config', async (req: FastifyRequest, reply: FastifyReply) => {
    const { id } = req.params as { id: string };
    const host = await loadHost(sql, id);
    if (!host) return reply.status(404).send({ error: 'host not found' });
    const target = sshTargetOf(host);
    if (!target || !host.config_path) {
      return reply.status(400).send({ error: 'host has no SSH config configured (set ssh_host/ssh_user/ssh_key_path/config_path first)' });
    }
    try {
      const content = await readRemoteConfig(target, host.config_path, exec, hostMode(host));
      return reply.send({ configPath: host.config_path, content });
    } catch (err) {
      return reply.status(502).send({ error: (err as Error).message });
    }
  });

  app.post('/api/hosts/:id/config/validate', async (req: FastifyRequest, reply: FastifyReply) => {
    const body = (req.body as Record<string, unknown>) ?? {};
    const content = body.content as string;
    if (typeof content !== 'string') {
      return reply.status(400).send({ error: 'content (string) is required' });
    }
    if (!schema) {
      return reply.status(500).send({ error: 'config schema not available on this host' });
    }
    const result = validateLlamaConfig(content, schema);
    return reply.send({ valid: result.valid, errors: result.errors });
  });

  app.post('/api/hosts/:id/config/diff', async (req: FastifyRequest, reply: FastifyReply) => {
    const { id } = req.params as { id: string };
    const body = (req.body as Record<string, unknown>) ?? {};
    const content = body.content as string;
    if (typeof content !== 'string') {
      return reply.status(400).send({ error: 'content (string) is required' });
    }
    const host = await loadHost(sql, id);
    if (!host) return reply.status(404).send({ error: 'host not found' });
    const target = sshTargetOf(host);
    if (!target || !host.config_path) {
      return reply.status(400).send({ error: 'host has no SSH config configured' });
    }
    try {
      const current = await readRemoteConfig(target, host.config_path, exec, hostMode(host));
      return reply.send({ diff: computeDiff(current, content) });
    } catch (err) {
      return reply.status(502).send({ error: (err as Error).message });
    }
  });

  app.post('/api/hosts/:id/config/apply', async (req: FastifyRequest, reply: FastifyReply) => {
    const { id } = req.params as { id: string };
    const body = (req.body as Record<string, unknown>) ?? {};
    const content = body.content as string;
    const confirm = body.confirm === true;
    if (typeof content !== 'string') {
      return reply.status(400).send({ error: 'content (string) is required' });
    }
    if (!confirm) {
      return reply.status(409).send({ error: 'apply requires confirmation', requiresConfirmation: true });
    }
    if (!schema) {
      return reply.status(500).send({ error: 'config schema not available on this host' });
    }
    const host = await loadHost(sql, id);
    if (!host) return reply.status(404).send({ error: 'host not found' });
    const target = sshTargetOf(host);
    const mode = hostMode(host);
    // restart_cmd is only used in shell mode; in wrapper mode the wrapper's
    // `restart` verb hardcodes the service, so restart_cmd is not required.
    if (!target || !host.config_path || (mode === 'shell' && !host.restart_cmd)) {
      return reply.status(400).send({ error: 'host needs ssh_host/ssh_user/ssh_key_path/config_path (+ restart_cmd in shell mode) set first' });
    }
    const baseUrl = resolveProviderBaseUrl(id);
    if (!baseUrl) {
      return reply.status(400).send({ error: `no base URL in registry for provider ${id}` });
    }

    const result = await applyRemoteConfig({
      target,
      configPath: host.config_path,
      restartCmd: host.restart_cmd ?? '',
      newConfig: content,
      schema,
      baseUrl,
      exec,
      mode,
    });

    const status = result.ok ? 200 : (result.step === 'validate' ? 400 : 502);
    return reply.status(status).send(result);
  });

  app.post('/api/hosts/:id/pull', async (req: FastifyRequest, reply: FastifyReply) => {
    const { id } = req.params as { id: string };
    const body = (req.body as Record<string, unknown>) ?? {};
    const repo = body.repo as string;
    const modelsDir = (body.modelsDir as string) ?? undefined;

    if (typeof repo !== 'string' || !validateRepoId(repo)) {
      return reply.status(400).send({ error: 'repo must be a valid HuggingFace id (org/name)' });
    }
    const host = await loadHost(sql, id);
    if (!host) return reply.status(404).send({ error: 'host not found' });
    const target = sshTargetOf(host);
    if (!target) {
      return reply.status(400).send({ error: 'host has no SSH configured' });
    }
    const mode = hostMode(host);
    if (mode === 'shell' && !modelsDir) {
      return reply.status(400).send({ error: 'shell-mode host requires a modelsDir in the request body' });
    }

    const jobId = `pull_${Date.now()}_${randomUUID().slice(0, 8)}`;
    const seq = fleet.hosts.get(id)?.seq ?? 0;
    // Fire and forget; progress streams over control_job frames.
    void runModelPull({ jobId, providerId: id, target, repo, mode, modelsDir }, exec, emitter, seq);

    return reply.status(202).send({ status: 'queued', jobId, repo });
  });
}

function hostMode(host: HostRow): SshMode {
  return host.ssh_mode === 'wrapper' ? 'wrapper' : 'shell';
}

interface HostRow {
  provider_id: string;
  ssh_host: string | null;
  ssh_user: string | null;
  ssh_key_path: string | null;
  config_path: string | null;
  restart_cmd: string | null;
  ssh_mode: string | null;
  os: string | null;
  gpu_label: string | null;
  enabled: boolean;
}

async function loadHost(sql: Sql, id: string): Promise<HostRow | null> {
  const rows = await sql<HostRow[]>`
    SELECT provider_id, ssh_host, ssh_user, ssh_key_path, config_path, restart_cmd, ssh_mode, os, gpu_label, enabled
    FROM control_hosts WHERE provider_id = ${id}
  `;
  return rows[0] ?? null;
}

function sshTargetOf(host: HostRow): SshTarget | null {
  if (!host.ssh_host || !host.ssh_user || !host.ssh_key_path) return null;
  return { host: host.ssh_host, user: host.ssh_user, keyPath: host.ssh_key_path };
}

/** Load the config schema from the configured path or the bundled copy. */
function loadConfigSchema(config: Config): object | null {
  const here = dirname(fileURLToPath(import.meta.url));
  // dist/routes/ssh-config.js -> dist/data/config-schema.json
  const bundled = resolve(here, '../data/config-schema.json');
  const path = config.LLAMA_CONFIG_SCHEMA_PATH ?? bundled;
  try {
    return JSON.parse(readFileSync(path, 'utf8'));
  } catch {
    if (path !== bundled) {
      try {
        return JSON.parse(readFileSync(bundled, 'utf8'));
      } catch {
        return null;
      }
    }
    return null;
  }
}
