/**
 * Org-admin Agent configuration routes (ADR-0017 / ADR-0018).
 *
 * Surface for browsing and editing Organization-scoped Agent roles, the skills
 * bound to them, and the model picker. The model list is **derived** from the
 * org's ACTIVE provider connection via OpenRouter's /v1/models API — there is
 * no separate model table to maintain. When no ACTIVE provider exists, the
 * route falls back to the env-default model registry so the admin surface
 * remains usable.
 *
 * Skill management (create, read, edit, disable) is performed in-band through
 * the deep module `OrganizationAgentConfiguration`, which writes to the same
 * content-addressed persistent store used by the host-console CLI. All skill
 * content flows through `importSkillFromFiles` → `inspectSkillDirectory` →
 * `commitSkillContent`, so the web path and CLI path share one ingestion
 * pipeline and one set of safety checks (SKILL.md manifest required, 512-file
 * / 16-byte limits, symlink rejection).
 */
import type { PrismaClient } from "@prisma/client";
import type { FastifyInstance } from "fastify";
import { OrganizationAgentConfiguration } from "../../agent/configuration.js";
import { ProviderModelCatalog } from "../../connections/providerModelCatalog.js";
import { createDefaultModelRegistry } from "../../settings/runtime.js";
import type { LocalSecretEnvelope } from "../../security/secretEnvelope.js";
import { requireOrgRole, type GuardDeps } from "../auth/guards.js";
import { handleRouteError } from "../errors.js";

export interface AgentConfigRouteConfig {
  readonly prisma: PrismaClient;
  readonly sessionSecret: string;
  readonly skillStoreRoot: string;
  readonly secretEnvelope: LocalSecretEnvelope;
}

export async function registerAgentConfigRoutes(
  app: FastifyInstance,
  config: AgentConfigRouteConfig,
): Promise<void> {
  const guardDeps: GuardDeps = { prisma: config.prisma, sessionSecret: config.sessionSecret };
  const agentConfig = new OrganizationAgentConfiguration(config.prisma, config.skillStoreRoot);
  const modelCatalog = new ProviderModelCatalog(config.prisma, config.secretEnvelope);

  app.get("/api/org/:orgSlug/agent-roles", async (request, reply) => {
    try {
      const { orgSlug } = request.params as { orgSlug: string };
      const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
      if (auth === null) return;
      const roles = await agentConfig.listRoles({ organizationId: auth.organization.id });
      return { roles };
    } catch (err) {
      return handleRouteError(reply, err);
    }
  });

  app.put("/api/org/:orgSlug/agent-roles/:roleId", async (request, reply) => {
    try {
      const { orgSlug, roleId } = request.params as { orgSlug: string; roleId: string };
      const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
      if (auth === null) return;
      const body = request.body as {
        label?: unknown;
        defaultModel?: unknown;
        systemPrompt?: unknown;
        tools?: unknown;
        sortOrder?: unknown;
        isDefault?: unknown;
      };
      if (typeof body.label !== "string" || body.label.trim() === "") {
        return reply.status(400).send({
          error: { code: "bad_request", message: "label is required" },
        });
      }
      const role = await agentConfig.upsertRole({
        organizationId: auth.organization.id,
        roleId,
        label: body.label,
        ...(body.defaultModel === null || typeof body.defaultModel === "string"
          ? { defaultModel: body.defaultModel as string | null }
          : {}),
        ...(body.systemPrompt === null || typeof body.systemPrompt === "string"
          ? { systemPrompt: body.systemPrompt as string | null }
          : {}),
        ...(body.tools === null || Array.isArray(body.tools)
          ? { tools: body.tools as readonly string[] | null }
          : {}),
        ...(typeof body.sortOrder === "number" ? { sortOrder: body.sortOrder } : {}),
        ...(typeof body.isDefault === "boolean" ? { isDefault: body.isDefault } : {}),
      });
      return role;
    } catch (err) {
      return handleRouteError(reply, err);
    }
  });

  app.put("/api/org/:orgSlug/agent-roles/:roleId/skills", async (request, reply) => {
    try {
      const { orgSlug, roleId } = request.params as { orgSlug: string; roleId: string };
      const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
      if (auth === null) return;
      const body = request.body as { skillNames?: unknown };
      if (!Array.isArray(body.skillNames) || body.skillNames.some((n) => typeof n !== "string")) {
        return reply.status(400).send({
          error: { code: "bad_request", message: "skillNames must be a string array" },
        });
      }
      await agentConfig.setRoleSkills({
        organizationId: auth.organization.id,
        roleId,
        skillNames: body.skillNames as readonly string[],
      });
      return { skillNames: body.skillNames as string[] };
    } catch (err) {
      return handleRouteError(reply, err);
    }
  });

  app.get("/api/org/:orgSlug/agent-skills", async (request, reply) => {
    try {
      const { orgSlug } = request.params as { orgSlug: string };
      const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
      if (auth === null) return;
      const skills = await agentConfig.listSkills({ organizationId: auth.organization.id });
      return { skills };
    } catch (err) {
      return handleRouteError(reply, err);
    }
  });

  app.get("/api/org/:orgSlug/agent-skills/:name/files", async (request, reply) => {
    try {
      const { orgSlug, name } = request.params as { orgSlug: string; name: string };
      const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
      if (auth === null) return;
      const skills = await agentConfig.listSkills({ organizationId: auth.organization.id });
      const skill = skills.find((s) => s.name === name && s.disabledAt === null);
      if (skill === undefined) {
        return reply.status(404).send({
          error: { code: "not_found", message: `skill not found: ${name}` },
        });
      }
      const files = await agentConfig.readSkillFiles({
        organizationId: auth.organization.id,
        contentDigest: skill.contentDigest,
      });
      return { files };
    } catch (err) {
      return handleRouteError(reply, err);
    }
  });

  app.put("/api/org/:orgSlug/agent-skills/:name", async (request, reply) => {
    try {
      const { orgSlug, name } = request.params as { orgSlug: string; name: string };
      const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
      if (auth === null) return;
      const body = request.body as { version?: unknown; files?: unknown };
      if (typeof body.version !== "string" || body.version.trim() === "") {
        return reply.status(400).send({
          error: { code: "bad_request", message: "version is required" },
        });
      }
      if (!Array.isArray(body.files) || body.files.length === 0) {
        return reply.status(400).send({
          error: { code: "bad_request", message: "files must be a non-empty array" },
        });
      }
      const skillFiles: Array<{ readonly path: string; readonly bytes: Buffer }> = [];
      for (const entry of body.files) {
        if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
          return reply.status(400).send({
            error: { code: "bad_request", message: "each file must be an object" },
          });
        }
        const e = entry as { path?: unknown; content?: unknown };
        if (typeof e.path !== "string" || typeof e.content !== "string") {
          return reply.status(400).send({
            error: { code: "bad_request", message: "each file needs string path and content" },
          });
        }
        skillFiles.push({ path: e.path, bytes: Buffer.from(e.content, "utf8") });
      }
      const result = await agentConfig.installSkillFromFiles({
        organizationId: auth.organization.id,
        files: skillFiles,
        version: body.version,
      });
      // The manifest name is authoritative — reject if it doesn't match the
      // URL param, so clients can't silently rename a skill under a different
      // route key.
      if (result.name !== name) {
        return reply.status(400).send({
          error: {
            code: "bad_request",
            message: `skill manifest name "${result.name}" does not match route "${name}"`,
          },
        });
      }
      return { id: result.id, name: result.name, contentDigest: result.contentDigest };
    } catch (err) {
      return handleRouteError(reply, err);
    }
  });

  app.patch("/api/org/:orgSlug/agent-skills/:name", async (request, reply) => {
    try {
      const { orgSlug, name } = request.params as { orgSlug: string; name: string };
      const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
      if (auth === null) return;
      const body = request.body as { description?: unknown; disabled?: unknown };
      if (body.disabled === true) {
        await agentConfig.disableSkill({ organizationId: auth.organization.id, name });
        return { disabled: true };
      }
      if (typeof body.description === "string") {
        await agentConfig.updateSkillDescription({
          organizationId: auth.organization.id,
          name,
          description: body.description,
        });
        return { updated: true };
      }
      return reply.status(400).send({
        error: { code: "bad_request", message: "provide description or disabled: true" },
      });
    } catch (err) {
      return handleRouteError(reply, err);
    }
  });

  app.get("/api/org/:orgSlug/agent-models", async (request, reply) => {
    try {
      const { orgSlug } = request.params as { orgSlug: string };
      const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
      if (auth === null) return;
      const providerModels = await modelCatalog.listModels(auth.organization.id);
      // Fall back to env-default registry when the org has no ACTIVE provider
      // (or the provider API is unreachable on first load).
      const models = providerModels.length > 0
        ? providerModels
        : createDefaultModelRegistry(process.env).listModels();
      return { models };
    } catch (err) {
      return handleRouteError(reply, err);
    }
  });
}
