// AskController.ts
import type { Request, Response } from 'express';

import type { ShapesMap } from '../shapes/shapes';
import type {
  AiService,
  GroupedItems,
  ItemsByCollection,
  SectionTitle,
} from '../ai/ai-service';

// Minimal shape of a "hit" produced by your search service.
// (Matches what your SHAPES.hydrate returns: collection + _id + optional fields.)
export type SearchHitBase = {
  collection: keyof ShapesMap | string;
  _id: string;
  title?: string | null;
};

export type HybridSearchServiceLike<
  THit extends SearchHitBase = SearchHitBase,
> = {
  hybridSearch: (query: string, kPerCollection?: number) => Promise<THit[]>;
};

export type AskControllerDeps<THit extends SearchHitBase = SearchHitBase> = {
  hybridSearchService: HybridSearchServiceLike<THit>;
  aiService: AiService;
  shapes: ShapesMap;
  activeCollections: string[];
};

export class AskController<THit extends SearchHitBase = SearchHitBase> {
  private hybridSearchService: HybridSearchServiceLike<THit>;
  private aiService: AiService;
  private shapes: ShapesMap;
  private activeCollections: string[];

  constructor({
    hybridSearchService,
    aiService,
    shapes,
    activeCollections,
  }: AskControllerDeps<THit>) {
    this.hybridSearchService = hybridSearchService;
    this.aiService = aiService;
    this.shapes = shapes;
    this.activeCollections = activeCollections;
  }

  clampK = (k: unknown): number => {
    const n = Number(k);
    if (!Number.isFinite(n)) return 6;
    return Math.max(1, Math.min(30, n));
  };

  buildContext = (hits: THit[], cap = 12): string => {
    const lines: string[] = [];
    let idx = 1;

    for (const h of hits.slice(0, cap)) {
      const col = h.collection as keyof ShapesMap;
      const shape = this.shapes[col];

      // If we have a shape, prefer its toContext formatter
      const line =
        shape?.toContext && typeof shape.toContext === 'function'
          ? // toContext expects the hydrated type, but THit is structurally compatible at runtime
            shape.toContext(h as any, idx)
          : `${idx}. [${String(h.collection)}#${h._id}] ${h.title ?? ''}`;

      lines.push(line);
      idx++;
    }

    return lines.join('\n\n');
  };

  handleAsk = async (req: Request, res: Response): Promise<Response> => {
    const q = (req.query.q || '').toString().trim();
    const kPerCollection = this.clampK(req.query.k ?? 6);

    if (!q) return res.status(400).json({ error: 'Missing q' });

    try {
      const hits = await this.hybridSearchService.hybridSearch(
        q,
        kPerCollection,
      );

      // itemsByCollection: used by AiService for titles + fallback answer
      const items: ItemsByCollection & GroupedItems = {};

      for (const name of this.activeCollections) items[name as any] = [];

      for (const h of hits) {
        const key = String(h.collection);
        if (!items[key as any]) items[key as any] = [];
        (items[key as any] as unknown[]).push(h);
      }

      const context = this.buildContext(hits, 12);
      const answer = await this.aiService.generateAnswer(
        q,
        hits,
        items,
        context,
      );

      const sectionsMeta: SectionTitle[] =
        await this.aiService.buildSectionTitles(q, items);
      const sections = sectionsMeta
        .map((s) => ({
          key: s.key,
          title: s.title,
          items: (items[s.key as any] as unknown[]) || [],
        }))
        .filter((sec) => sec.items.length);

      return res.json({
        query: q,
        answer,
        items,
        sections,
        results: hits,
      });
    } catch (e) {
      console.error(e);
      return res.status(500).json({ error: 'ask_failed' });
    }
  };

  health = (_req: Request, res: Response): Response => res.send('OK');
}
