import type { FastifyInstance } from 'fastify';
import { createPatch } from 'diff';

import type { ResourceRevisionService } from '../services/resource-revision.service.js';
import type { PromptService } from '../services/prompt.service.js';
import type { ResourceType } from '../repositories/resource-revision.repository.js';

interface RevisionRouteDeps {
  revisionService: ResourceRevisionService;
  promptService: PromptService;
  // Future: skillService for PR-3.
}

const VALID_TYPES: readonly ResourceType[] = ['prompt', 'skill'] as const;

export function registerRevisionRoutes(app: FastifyInstance, deps: RevisionRouteDeps): void {
  const { revisionService, promptService } = deps;

  // List history for a resource. Either both query params or none (none = error).
  app.get<{ Querystring: { resourceType?: string; resourceId?: string; limit?: string } }>(
    '/api/v1/revisions',
    async (request) => {
      const { resourceType, resourceId, limit } = request.query;
      if (!resourceType || !resourceId) {
        throw Object.assign(
          new Error('Both resourceType and resourceId are required'),
          { statusCode: 400 },
        );
      }
      if (!VALID_TYPES.includes(resourceType as ResourceType)) {
        throw Object.assign(
          new Error(`Invalid resourceType: ${resourceType}`),
          { statusCode: 400 },
        );
      }
      const limitNum = limit ? Math.min(500, Math.max(1, Number(limit))) : 100;
      return revisionService.listHistory(resourceType as ResourceType, resourceId, limitNum);
    },
  );

  app.get<{ Params: { id: string } }>('/api/v1/revisions/:id', async (request) => {
    const revision = await revisionService.getById(request.params.id);
    if (revision === null) {
      throw Object.assign(new Error(`Revision not found: ${request.params.id}`), { statusCode: 404 });
    }
    return revision;
  });

  /**
   * Unified diff between two revisions, or between a revision and the
   * live resource body. `against` accepts another revision id or the
   * literal string `live`.
   */
  app.get<{ Params: { id: string }; Querystring: { against?: string } }>(
    '/api/v1/revisions/:id/diff',
    async (request) => {
      const revision = await revisionService.getById(request.params.id);
      if (revision === null) {
        throw Object.assign(new Error(`Revision not found: ${request.params.id}`), { statusCode: 404 });
      }
      const against = request.query.against ?? 'live';

      let otherContent: string;
      let otherLabel: string;
      if (against === 'live') {
        // For prompts, fetch the live row by resourceId.
        if (revision.resourceType === 'prompt') {
          const prompt = await promptService.getPrompt(revision.resourceId);
          otherContent = prompt.content;
          otherLabel = `${prompt.name} (live, semver ${prompt.semver})`;
        } else {
          // PR-3 will wire skillService here.
          throw Object.assign(
            new Error(`Live diff not supported for resourceType ${revision.resourceType} yet`),
            { statusCode: 501 },
          );
        }
      } else {
        const otherRev = await revisionService.getById(against);
        if (otherRev === null) {
          throw Object.assign(new Error(`Other revision not found: ${against}`), { statusCode: 404 });
        }
        if (otherRev.resourceType !== revision.resourceType || otherRev.resourceId !== revision.resourceId) {
          throw Object.assign(
            new Error('Diff requires both revisions to be of the same resource'),
            { statusCode: 400 },
          );
        }
        otherContent = stringContent(otherRev.body);
        otherLabel = `revision ${otherRev.id} (${otherRev.semver})`;
      }

      const thisContent = stringContent(revision.body);
      const thisLabel = `revision ${revision.id} (${revision.semver})`;

      // Unified-format patch. Caller can render this directly or pass to a diff viewer.
      const patch = createPatch(`${revision.resourceType}/${revision.resourceId}`, otherContent, thisContent, otherLabel, thisLabel);
      return { patch };
    },
  );

  // POST /api/v1/prompts/:id/restore-revision { revisionId, note? }
  // (Skill route registered in PR-3 alongside this with the same shape.)
  app.post<{ Params: { id: string }; Body: { revisionId: string; note?: string } }>(
    '/api/v1/prompts/:id/restore-revision',
    async (request) => {
      const { revisionId, note } = request.body;
      if (!revisionId) {
        throw Object.assign(new Error('revisionId is required'), { statusCode: 400 });
      }
      return promptService.restoreRevisionForPrompt(request.params.id, revisionId, note);
    },
  );
}

/** Pull a `content` string out of a revision body, falling back to the raw JSON. */
function stringContent(body: unknown): string {
  if (body !== null && typeof body === 'object' && !Array.isArray(body)) {
    const v = (body as Record<string, unknown>)['content'];
    if (typeof v === 'string') return v;
  }
  return JSON.stringify(body, null, 2);
}
