import type { FastifyInstance } from 'fastify';
import { z } from 'zod';
import type { Sql } from '../db.js';
import {
  listPending,
  applyOne,
  applyAll,
  rejectOne,
  rewindOne,
  queueCreate,
} from '../services/pending_changes.js';
import { WriteGuardError } from '../services/write_guard.js';
import { rebaselineWorktreeAfterApply } from '../services/worktrees.js';

const CreateBody = z.object({
  file_path: z.string().min(1),
  content: z.string(),
});

/**
 * Resolve project root from a session's project path.
 */
async function resolveProjectRoot(sql: Sql, sessionId: string): Promise<string | null> {
  const rows = await sql<{ path: string }[]>`
    SELECT p.path FROM sessions s
    JOIN projects p ON s.project_id = p.id
    WHERE s.id = ${sessionId}
  `;
  return rows.length > 0 ? rows[0]!.path : null;
}

/**
 * Resolve project root from a pending change's session.
 */
async function resolveProjectRootForChange(sql: Sql, changeId: string): Promise<string | null> {
  const rows = await sql<{ path: string }[]>`
    SELECT p.path FROM pending_changes pc
    JOIN sessions s ON pc.session_id = s.id
    JOIN projects p ON s.project_id = p.id
    WHERE pc.id = ${changeId}
  `;
  return rows.length > 0 ? rows[0]!.path : null;
}

export function registerPendingRoutes(app: FastifyInstance, sql: Sql): void {
  // GET /api/sessions/:sessionId/pending — list pending changes for a session
  app.get<{ Params: { sessionId: string } }>(
    '/api/sessions/:sessionId/pending',
    async (req, reply) => {
      const sessionId = req.params.sessionId;

      const session = await sql<{ id: string }[]>`SELECT id FROM sessions WHERE id = ${sessionId}`;
      if (session.length === 0) {
        reply.code(404);
        return { error: 'session not found' };
      }

      const pending = await listPending(sql, sessionId);
      return pending;
    },
  );

  app.post<{ Params: { sessionId: string } }>(
    '/api/sessions/:sessionId/pending/create',
    async (req, reply) => {
      const sessionId = req.params.sessionId;

      const parsed = CreateBody.safeParse(req.body);
      if (!parsed.success) {
        reply.code(400);
        return { error: 'invalid body', details: parsed.error.flatten() };
      }

      const projectRoot = await resolveProjectRoot(sql, sessionId);
      if (!projectRoot) {
        reply.code(404);
        return { error: 'session or project not found' };
      }

      try {
        const change = await queueCreate(
          sql,
          sessionId,
          null,
          parsed.data.file_path,
          parsed.data.content,
          projectRoot,
          // Manual RightRail create — no agent staged it; renders as "manual".
          null,
        );
        return change;
      } catch (err) {
        if (err instanceof WriteGuardError) {
          reply.code(422);
          return { error: err.message };
        }
        throw err;
      }
    },
  );

  // POST /api/sessions/:sessionId/pending/apply — apply all pending changes
  app.post<{ Params: { sessionId: string } }>(
    '/api/sessions/:sessionId/pending/apply',
    async (req, reply) => {
      const sessionId = req.params.sessionId;

      const projectRoot = await resolveProjectRoot(sql, sessionId);
      if (!projectRoot) {
        reply.code(404);
        return { error: 'session or project not found' };
      }

      const results = await applyAll(sql, sessionId, projectRoot);

      // v2.6 Phase 3 (3.5): re-baseline the session worktree's diff to the applied
      // state, so the next external-agent turn diffs against applied-not-original
      // and doesn't re-surface the just-applied changes. Best-effort: a worktree
      // session may not exist (native-only chat), and a re-baseline hiccup must not
      // fail the apply the user just requested.
      if (results.some((r) => r.success)) {
        await rebaselineWorktreeAfterApply(sql, sessionId).catch(() => {});
      }
      return { results };
    },
  );

  // POST /api/pending/:id/apply — apply a single pending change
  app.post<{ Params: { id: string } }>(
    '/api/pending/:id/apply',
    async (req, reply) => {
      const changeId = req.params.id;

      const projectRoot = await resolveProjectRootForChange(sql, changeId);
      if (!projectRoot) {
        reply.code(404);
        return { error: 'pending change or project not found' };
      }

      const result = await applyOne(sql, changeId, projectRoot);
      if (!result.success) {
        reply.code(422);
      } else {
        // v2.6 Phase 3 (3.5): re-baseline the session worktree after a successful
        // apply so the next external-agent turn diffs against applied-not-original.
        // Resolve the change's session; best-effort, never fails the apply.
        const sessRows = await sql<{ session_id: string }[]>`
          SELECT session_id FROM pending_changes WHERE id = ${changeId}
        `;
        const sessionId = sessRows[0]?.session_id;
        if (sessionId) await rebaselineWorktreeAfterApply(sql, sessionId).catch(() => {});
      }
      return result;
    },
  );

  // POST /api/pending/:id/reject — reject a single pending change
  app.post<{ Params: { id: string } }>(
    '/api/pending/:id/reject',
    async (req, _reply) => {
      const changeId = req.params.id;

      await rejectOne(sql, changeId);
      return { ok: true };
    },
  );

  // POST /api/pending/:id/rewind — rewind (undo) an applied change
  app.post<{ Params: { id: string } }>(
    '/api/pending/:id/rewind',
    async (req, reply) => {
      const changeId = req.params.id;

      const projectRoot = await resolveProjectRootForChange(sql, changeId);
      if (!projectRoot) {
        reply.code(404);
        return { error: 'pending change or project not found' };
      }

      const result = await rewindOne(sql, changeId, projectRoot);
      if (!result.success) {
        reply.code(422);
      }
      return result;
    },
  );
}
