import type { FastifyInstance } from 'fastify';
import { eq, desc } from 'drizzle-orm';
import { db } from '../db.js';
import { recommendations, feedback } from '../db/schema.js';
import { runPipeline } from '../pipelines/recommendation.js';
import { runContinuousPipeline } from '../pipelines/continuous.js';
import type { MediaType, SSEEvent } from '../types/agents.js';
import { supportsWebSearch } from '../agent.js';
import {
  getPipelineHistory,
  hasPipelineSession,
  publishPipelineEvent,
  resetPipelineSession,
  subscribeToPipelineEvents,
} from '../pipelineStreams.js';

const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

function isUuid(id: string): boolean {
  return UUID_RE.test(id);
}

function writeSSE(raw: typeof import('node:http').ServerResponse.prototype, event: SSEEvent): void {
  raw.write(`data: ${JSON.stringify(event)}\n\n`);
}

export default async function recommendationsRoute(fastify: FastifyInstance) {
  // POST /recommendations — create record, return { id }
  fastify.post('/recommendations', async (request, reply) => {
    const body = request.body as {
      main_prompt: string;
      liked_shows?: string;
      disliked_shows?: string;
      themes?: string;
      brainstorm_count?: number;
      media_type?: string;
      use_web_search?: boolean;
      use_validator?: boolean;
      hard_requirements?: boolean;
      self_expansive?: boolean;
      expansive_passes?: number;
      expansive_mode?: string;
    };

    const title = (body.main_prompt ?? '')
      .trim()
      .split(/\s+/)
      .slice(0, 5)
      .join(' ');

    const rawCount = Number(body.brainstorm_count ?? 100);
    const brainstorm_count = Number.isFinite(rawCount) ? Math.min(200, Math.max(50, rawCount)) : 100;
    const media_type: MediaType = body.media_type === 'movie' ? 'movie' : 'tv_show';
    const use_web_search = body.use_web_search === true;
    const use_validator = body.use_validator === true && supportsWebSearch;
    const hard_requirements = body.hard_requirements === true;
    const self_expansive = body.self_expansive === true;
    const rawPasses = Number(body.expansive_passes ?? 2);
    const expansive_passes = Number.isFinite(rawPasses) ? Math.min(5, Math.max(1, rawPasses)) : 2;
    const expansive_mode = body.expansive_mode === 'extreme' ? 'extreme' : 'soft';

    const [rec] = await db
      .insert(recommendations)
      .values({
        title: title || 'Untitled',
        main_prompt: body.main_prompt ?? '',
        liked_shows: body.liked_shows ?? '',
        disliked_shows: body.disliked_shows ?? '',
        themes: body.themes ?? '',
        brainstorm_count,
        media_type,
        use_web_search,
        use_validator,
        hard_requirements,
        self_expansive,
        expansive_passes,
        expansive_mode,
        status: 'pending',
      })
      .returning({ id: recommendations.id });

    return reply.code(201).send({ id: rec?.id });
  });

  // POST /recommendations/continuous — create record and run continuous pipeline with SSE
  fastify.post('/recommendations/continuous', async (request, reply) => {
    const body = request.body as {
      liked_shows: string;
      disliked_shows?: string;
      themes?: string;
      requirements?: string;
      avoid?: string;
      total_count: number;
      media_type?: string;
      use_web_search?: boolean;
      validate_results?: boolean;
    };

    const mediaType: MediaType = body.media_type === 'movie' ? 'movie' : 'tv_show';
    const useWebSearch = body.use_web_search === true;
    const validateResults = body.validate_results === true && supportsWebSearch;
    const totalCount = Number.isFinite(body.total_count) ? Math.min(100, Math.max(10, body.total_count)) : 30;

    // Create record first with pending status
    const title = body.themes?.trim().split(/\s+/).slice(0, 5).join(' ') || 'Continuous Recommendations';
    const [rec] = await db
      .insert(recommendations)
      .values({
        title,
        main_prompt: body.themes ?? 'Continuous recommendations',
        liked_shows: body.liked_shows,
        disliked_shows: body.disliked_shows ?? '',
        themes: body.themes ?? '',
        brainstorm_count: totalCount,
        media_type: mediaType,
        use_web_search: useWebSearch,
        use_validator: validateResults,
        status: 'pending',
      })
      .returning({ id: recommendations.id });

    if (!rec) {
      return reply.code(500).send({ error: 'Failed to create recommendation record' });
    }

    const recId = rec.id;
    resetPipelineSession(recId);

    // Set SSE headers and hijack
    reply.raw.setHeader('Content-Type', 'text/event-stream');
    reply.raw.setHeader('Cache-Control', 'no-cache');
    reply.raw.setHeader('Connection', 'keep-alive');
    reply.raw.setHeader('Access-Control-Allow-Origin', '*');
    reply.raw.flushHeaders();
    reply.hijack();

    // Immediately send the record ID so frontend can navigate to it
    reply.raw.write(`data: ${JSON.stringify({ type: 'created', id: recId })}\n\n`);

    const sseWrite = (event: SSEEvent) => {
      publishPipelineEvent(recId, event);
      try {
        writeSSE(reply.raw, event);
      } catch {
        // Client disconnected
      }
    };

    try {
      // Update status to running
      await db
        .update(recommendations)
        .set({ status: 'running' })
        .where(eq(recommendations.id, recId));

      // Run the continuous pipeline (it will now update the existing record)
      await runContinuousPipeline(recId, {
        likedShows: body.liked_shows,
        dislikedShows: body.disliked_shows ?? '',
        themes: body.themes ?? '',
        requirements: body.requirements ?? '',
        avoid: body.avoid ?? '',
        totalCount,
        useWebSearch,
        validateResults,
        mediaType,
      }, sseWrite);
    } finally {
      reply.raw.end();
    }
  });

  // GET /recommendations — list all
  fastify.get('/recommendations', async (_request, reply) => {
    const rows = await db
      .select({
        id: recommendations.id,
        title: recommendations.title,
        status: recommendations.status,
        media_type: recommendations.media_type,
        created_at: recommendations.created_at,
      })
      .from(recommendations)
      .orderBy(desc(recommendations.created_at));
    return reply.send(rows);
  });

  // GET /recommendations/:id — full record
  fastify.get('/recommendations/:id', async (request, reply) => {
    const { id } = request.params as { id: string };
    if (!isUuid(id)) return reply.code(404).send({ error: 'Not found' });
    const [rec] = await db
      .select()
      .from(recommendations)
      .where(eq(recommendations.id, id));

    if (!rec) return reply.code(404).send({ error: 'Not found' });
    return reply.send(rec);
  });

  // GET /recommendations/:id/stream — SSE pipeline stream
  fastify.get('/recommendations/:id/stream', async (request, reply) => {
    const { id } = request.params as { id: string };
    if (!isUuid(id)) return reply.code(404).send({ error: 'Not found' });
    const [rec] = await db
      .select()
      .from(recommendations)
      .where(eq(recommendations.id, id));

    if (!rec) return reply.code(404).send({ error: 'Not found' });

    // Set SSE headers and hijack before any branching
    reply.raw.setHeader('Content-Type', 'text/event-stream');
    reply.raw.setHeader('Cache-Control', 'no-cache');
    reply.raw.setHeader('Connection', 'keep-alive');
    reply.raw.setHeader('Access-Control-Allow-Origin', '*');
    reply.raw.flushHeaders();
    reply.hijack();

    // Resilient write — swallows errors so a disconnected client never crashes
    // an in-flight pipeline that is still running server-side.
    const sseWrite = (event: SSEEvent) => {
      try {
        writeSSE(reply.raw, event);
      } catch {
        // Client disconnected — pipeline continues, writes are silently dropped
      }
    };

    try {
      // Already finished — send a synthetic completion event and close immediately.
      if (rec.status === 'done') {
        sseWrite({ stage: 'complete', status: 'done', data: { title: rec.title } });
        return;
      }

      // Already errored — send a synthetic error event and close immediately.
      if (rec.status === 'error') {
        sseWrite({ stage: 'curator', status: 'error', data: { message: 'Pipeline failed' } });
        return;
      }

      // Already running — the pipeline is executing on a previous connection.
      // Poll the DB until it reaches a terminal state, then report the result.
      // This prevents starting a duplicate pipeline run on page reload.
      if (rec.status === 'running') {
        if (hasPipelineSession(id)) {
          const history = getPipelineHistory(id);
          for (const event of history) {
            sseWrite(event);
          }

          const lastEvent = history[history.length - 1];
          if (lastEvent && (lastEvent.stage === 'complete' || lastEvent.status === 'error')) {
            return;
          }

          await new Promise<void>((resolve) => {
            const unsubscribe = subscribeToPipelineEvents(id, (event) => {
              sseWrite(event);
              if (event.stage === 'complete' || event.status === 'error') {
                unsubscribe();
                resolve();
              }
            });

            request.raw.on('close', () => {
              unsubscribe();
              resolve();
            });
          });
          return;
        }

        const POLL_INTERVAL_MS = 2000;
        const TIMEOUT_MS = 20 * 60 * 1000; // 20 minutes hard ceiling
        const start = Date.now();
        while (Date.now() - start < TIMEOUT_MS) {
          await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
          const [current] = await db
            .select({ status: recommendations.status, title: recommendations.title })
            .from(recommendations)
            .where(eq(recommendations.id, id));
          if (!current || current.status === 'done') {
            sseWrite({ stage: 'complete', status: 'done', data: { title: current?.title ?? rec.title } });
            return;
          }
          if (current.status === 'error') {
            sseWrite({ stage: 'curator', status: 'error', data: { message: 'Pipeline failed' } });
            return;
          }
          // Still running — keep polling
        }
        // Timed out waiting — report as error
        sseWrite({ stage: 'curator', status: 'error', data: { message: 'Pipeline timed out' } });
        return;
      }

      // status === 'pending' — start the pipeline normally.
      resetPipelineSession(id);

      // Load all feedback to potentially inject as context
      const feedbackRows = await db.select().from(feedback);
      const mediaLabel = rec.media_type === 'movie' ? 'Movie' : 'Show';
      const feedbackContext =
        feedbackRows.length > 0
          ? feedbackRows
              .map(
                (f) =>
                  `${mediaLabel}: "${f.item_name}" — Rating: ${f.stars}/3 stars${f.feedback ? ` — Comment: ${f.feedback}` : ''}`,
              )
              .join('\n')
          : undefined;

      await runPipeline(rec, (event) => {
        publishPipelineEvent(id, event);
        sseWrite(event);
      }, feedbackContext);
    } finally {
      reply.raw.end();
    }
  });

  // DELETE /recommendations/:id — delete a recommendation
  fastify.delete('/recommendations/:id', async (request, reply) => {
    const { id } = request.params as { id: string };
    if (!isUuid(id)) return reply.code(404).send({ error: 'Not found' });
    const [rec] = await db
      .select({ id: recommendations.id })
      .from(recommendations)
      .where(eq(recommendations.id, id));

    if (!rec) return reply.code(404).send({ error: 'Not found' });

    await db.delete(recommendations).where(eq(recommendations.id, id));

    return reply.send({ ok: true });
  });

  // POST /recommendations/:id/rerank — reset status so client can re-open SSE stream
  fastify.post('/recommendations/:id/rerank', async (request, reply) => {
    const { id } = request.params as { id: string };
    if (!isUuid(id)) return reply.code(404).send({ error: 'Not found' });
    const [rec] = await db
      .select({ id: recommendations.id })
      .from(recommendations)
      .where(eq(recommendations.id, id));

    if (!rec) return reply.code(404).send({ error: 'Not found' });

    await db
      .update(recommendations)
      .set({ status: 'pending' })
      .where(eq(recommendations.id, id));
    resetPipelineSession(id);

    return reply.send({ ok: true });
  });
}
