import { NextRequest, NextResponse } from 'next/server';

import { withAuth } from '@/lib/api/helpers';
import { errorResponse } from '@/lib/errors';
import { search, saveRecentSearch, getRecentlyTouchedIds } from '@/lib/services/search.service';
import { searchQuerySchema } from '@/lib/validators/search';

export const GET = withAuth(async (req: NextRequest, ctx) => {
  try {
    const params = req.nextUrl.searchParams;
    const parsed = searchQuerySchema.parse({
      q: params.get('q') ?? '',
      type: params.get('type') ?? undefined,
      limit: params.get('limit') ?? undefined,
      includeOtherPorts: params.get('includeOtherPorts') ?? undefined,
    });

    // Only super admins can opt into cross-port search; silently ignored
    // otherwise so a non-admin can't probe whether the flag exists.
    const includeOtherPorts = ctx.isSuperAdmin && (parsed.includeOtherPorts ?? false);

    // Run the affinity-set query alongside the search so it costs us
    // nothing in wall-clock time. The set is small (capped at 200 ids)
    // and is used to boost ranking inside each bucket.
    const [touchedIds, results] = await Promise.all([
      getRecentlyTouchedIds(ctx.userId, ctx.portId).catch(() => new Set<string>()),
      search(ctx.portId, parsed.q, {
        permissions: ctx.permissions,
        isSuperAdmin: ctx.isSuperAdmin,
        limit: parsed.limit,
        type: parsed.type,
        includeOtherPorts,
      }),
    ]);

    // Resolve `:portSlug` placeholders in the nav-catalog hrefs. Done
    // here (not in the service) so the service stays portSlug-free -
    // it only knows portId, which is the right tenant boundary anyway.
    if (results.navigation.length > 0) {
      results.navigation = results.navigation.map((n) => ({
        ...n,
        href: n.href.replace(':portSlug', ctx.portSlug),
        id: n.href.replace(':portSlug', ctx.portSlug),
      }));
    }

    // Re-run affinity sort with the now-resolved set. The service can
    // also accept this up front, but we kick off both queries in
    // parallel for latency, then apply on results - affinity is just a
    // post-sort, so order of operations doesn't change correctness.
    if (touchedIds.size > 0) {
      const reorder = <T extends { id: string }>(rows: T[]) => {
        const indexed = rows.map((row, idx) => ({ row, idx, hit: touchedIds.has(row.id) }));
        indexed.sort((a, b) => Number(b.hit) - Number(a.hit) || a.idx - b.idx);
        return indexed.map((x) => x.row);
      };
      results.clients = reorder(results.clients);
      results.residentialClients = reorder(results.residentialClients);
      results.yachts = reorder(results.yachts);
      results.companies = reorder(results.companies);
      results.interests = reorder(results.interests);
      results.residentialInterests = reorder(results.residentialInterests);
      results.berths = reorder(results.berths);
      results.invoices = reorder(results.invoices);
      results.expenses = reorder(results.expenses);
      results.documents = reorder(results.documents);
      results.files = reorder(results.files);
      results.reminders = reorder(results.reminders);
    }

    // Fire-and-forget - recent search history is non-critical.
    if (parsed.q.length >= 2) {
      saveRecentSearch(ctx.userId, ctx.portId, parsed.q);
    }

    // L34 carve-out: deliberate bare-shape response (NOT the `{ data }`
    // envelope). The global-search palette reads the grouped result fields
    // off the JSON root; wrapping them would break the consumer. Left as-is.
    return NextResponse.json(results);
  } catch (error) {
    return errorResponse(error);
  }
});
