import type { FastifyInstance } from 'fastify';
import { z } from 'zod';
import { sanitizeId, tmuxSessionName, capturePane } from '../pty/manager.js';
import { searchRingBuffer } from "../pty/registry.js";

const ParamsSchema = z.object({
  sid: z.string(),
  pid: z.string(),
});

const MAX_PATTERN_LENGTH = 200;

// Zod-refined string: reject empty and overly-long patterns to prevent ReDoS
const PatternQuerySchema = z
  .string()
  .min(1, 'pattern is required')
  .max(MAX_PATTERN_LENGTH, `pattern must not exceed ${MAX_PATTERN_LENGTH} characters`);

const QuerySchema = z.object({
  pattern: PatternQuerySchema,
  limit: z.coerce.number().int().min(1).max(500).default(50),
  context: z.coerce.number().int().min(0).max(50).default(0),
});

interface SearchMatch {
  line: number;
  content: string;
  contextBefore: string[];
  contextAfter: string[];
}


/**
 * Search a captured pane buffer using a regex. This is the fallback path
 * when the ring buffer doesn't have enough matches.
 */
function grepBuffer(
  text: string,
  pattern: string,
  limit: number,
  context: number,
): SearchMatch[] {
  let re: RegExp;
  try {
    re = new RegExp(pattern, 'u');
  } catch {
    return [];
  }

  const lines = text.split('\n');
  const results: SearchMatch[] = [];

  for (let i = 0; i < lines.length; i++) {
    if (results.length >= limit) break;
    if (re.test(lines[i]!)) {
      const contextBefore: string[] = [];
      const contextAfter: string[] = [];
      for (let c = 1; c <= context; c++) {
        const ci = i - c;
        if (ci >= 0) contextBefore.unshift(lines[ci]!);
      }
      for (let c = 1; c <= context; c++) {
        const ci = i + c;
        if (ci < lines.length) contextAfter.push(lines[ci]!);
      }
      results.push({
        line: i + 1,
        content: lines[i]!,
        contextBefore,
        contextAfter,
      });
    }
  }

  return results;
}

export function registerSearchRoutes(app: FastifyInstance, tmuxConfPath: string): void {
  app.get<{
    Params: { sid: string; pid: string };
    Querystring: { pattern?: string; limit?: string; context?: string };
  }>(
    '/api/term/sessions/:sid/panes/:pid/search',
    async (req, reply) => {
      const p = ParamsSchema.safeParse(req.params);
      if (!p.success) return reply.code(400).send({ error: 'bad_params' });

      const sid = sanitizeId(p.data.sid);
      const pid = sanitizeId(p.data.pid);
      if (!sid || !pid) return reply.code(400).send({ error: 'bad_id_format' });

      const q = QuerySchema.safeParse(req.query);
      if (!q.success) {
        return reply.code(400).send({
          error: 'bad_query',
          details: q.error.flatten().fieldErrors,
        });
      }

      const { pattern, limit, context } = q.data;

      // ── Path 1: ring buffer search (fast, no tmux interaction) ──
      const ringMatches = searchRingBuffer(pid, pattern, { limit, context });
      if (ringMatches.length >= limit) {
        return reply.code(200).send({
          matches: ringMatches,
          total: ringMatches.length,
          truncated: ringMatches.length >= limit,
          source: 'ring' as const,
        });
      }

      // ── Path 2: capture-pane + grep fallback (10s timeout) ──
      const sessionName = tmuxSessionName(pid);

      let capture: string;
      try {
        capture = await withTimeout(
          capturePane(tmuxConfPath, sessionName, 5000),
          10_000,
        );
      } catch (err) {
        req.log.warn({ err, pid }, 'capture-pane timed out or failed');
        return reply.code(200).send({
          matches: ringMatches,
          total: ringMatches.length,
          truncated: false,
          source: 'ring' as const,
        });
      }

      if (!capture) {
        // tmux pane may no longer exist — return whatever ring had
        return reply.code(200).send({
          matches: ringMatches,
          total: ringMatches.length,
          truncated: false,
          source: 'ring' as const,
        });
      }

      const captureMatches = grepBuffer(capture, pattern, limit, context);

      return reply.code(200).send({
        matches: captureMatches,
        total: captureMatches.length,
        truncated: captureMatches.length >= limit,
        source: 'capture' as const,
      });
    },
  );
}

function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
  return Promise.race([
    promise,
    new Promise<never>((_, reject) =>
      setTimeout(() => reject(new Error('timeout')), ms),
    ),
  ]);
}
