import type { FastifyInstance } from 'fastify';
import { z } from 'zod';
import type { Sql } from '../db.js';
import type { Config } from '../config.js';
import {
  getProviderSnapshot,
  clearProviderSnapshotCache,
  peekSnapshotEntry,
} from '../services/provider-snapshot.js';
import {
  load,
  save,
  CoderProvidersFileSchema,
  ProviderConfigPatchSchema,
  mergeProviderConfigPatch,
} from '../services/provider-config.js';
import {
  reloadProviderConfig,
  getResolvedRegistry,
} from '../services/provider-config-registry.js';
import {
  getProviderDiagnostic,
  type DiagnosticAgentRow,
} from '../services/provider-diagnostic.js';

const RefreshBodySchema = z.object({ providers: z.array(z.string()).optional() });

export function registerProviderRoutes(app: FastifyInstance, sql: Sql, config: Config): void {
  app.get<{ Querystring: { cwd?: string } }>('/api/providers/snapshot', async (req, _reply) => {
    const cwd = req.query.cwd;
    return getProviderSnapshot(sql, config, cwd);
  });

  // 4.1 — current loaded config file (raw CoderProvidersFile, not the resolved registry).
  app.get('/api/providers/config', async (_req, _reply) => {
    return load(config.CODER_PROVIDERS_PATH);
  });

  // 4.2 — patch the config file (design.md §6.2). Strict order is the whole
  // correctness story: validate → save → reload → clear. A malformed body or an
  // invalid merged result returns 422 and NEVER writes; a save failure returns
  // 500 and leaves in-memory state untouched (no file/registry divergence).
  app.patch('/api/providers/config', async (req, reply) => {
    // 1. Validate the PATCH body shape (malformed → 422, never reaches merge).
    const parsed = ProviderConfigPatchSchema.safeParse(req.body);
    if (!parsed.success) {
      return reply.code(422).send({
        error: 'invalid provider config patch',
        issues: parsed.error.flatten(),
      });
    }

    // 2. Shallow per-id merge over the current file (null deletes; object replaces).
    const current = load(config.CODER_PROVIDERS_PATH);
    const merged = mergeProviderConfigPatch(current, parsed.data);

    // 3. Validate the merged result — refuse to write a config that won't load.
    const validated = CoderProvidersFileSchema.safeParse(merged);
    if (!validated.success) {
      return reply.code(422).send({
        error: 'merged provider config is invalid',
        issues: validated.error.flatten(),
      });
    }

    // 4. Persist. If save throws, STOP here — do NOT reload/clear, so the file on
    // disk and the in-memory resolved registry can never diverge.
    try {
      save(config.CODER_PROVIDERS_PATH, validated.data);
    } catch (err) {
      req.log.error(
        { err: err instanceof Error ? err.message : String(err), path: config.CODER_PROVIDERS_PATH },
        'provider-config: save failed — in-memory state untouched',
      );
      return reply.code(500).send({ error: 'failed to write provider config' });
    }

    // 5 + 6. Rebuild the in-memory resolved registry from the new file, then drop
    // the snapshot cache so the next /snapshot reflects the change.
    reloadProviderConfig();
    clearProviderSnapshotCache();

    // 7. Return the new config (per §6.2 `{ ok: true }`, plus the merged providers
    // so the client can update without a follow-up GET).
    return { ok: true, providers: validated.data.providers };
  });

  // 4.3 — force a cold probe. Optional { providers?: string[] } narrows the
  // reported subset (design.md §6.3 Paseo pattern). The force=true snapshot is
  // the only existing re-probe primitive (per-provider force would be a
  // snapshot-internal change, out of Phase 4 scope), so the probe runs for all
  // installed providers; the `refreshed` count reflects the requested subset.
  app.post('/api/providers/refresh', async (req, reply) => {
    const parsed = RefreshBodySchema.safeParse(req.body ?? {});
    if (!parsed.success) {
      return reply.code(422).send({ error: 'invalid refresh body', issues: parsed.error.flatten() });
    }
    const subset = parsed.data.providers;
    clearProviderSnapshotCache();
    const entries = await getProviderSnapshot(sql, config, undefined, true);
    const refreshed =
      subset && subset.length > 0
        ? entries.filter((e) => subset.includes(e.name)).length
        : entries.length;
    return { refreshed };
  });

  // 4.4 — per-provider diagnostic (design.md §6.4 → JSON `{ diagnostic: string }`).
  // Read-only: reports cached state (resolved def + available_agents row + warm
  // snapshot cache for the last probe error) plus a `which` PATH check. No probe
  // spawn. The report itself is a plaintext block (§8); the route wraps it as JSON.
  app.get<{ Params: { id: string } }>('/api/providers/:id/diagnostic', async (req, reply) => {
    const id = req.params.id;
    const resolved = getResolvedRegistry().get(id);
    if (!resolved) {
      return reply.code(404).send({ error: `unknown provider '${id}'` });
    }
    const rows = await sql<DiagnosticAgentRow[]>`
      SELECT name, install_path, supports_acp, models, last_probed_at
      FROM available_agents WHERE name = ${id}
    `;
    const report = await getProviderDiagnostic(resolved, rows[0], {
      cachedEntry: peekSnapshotEntry(id),
    });
    return { diagnostic: report };
  });
}
