import { NextResponse } from 'next/server';
import { and, eq, inArray, isNull } from 'drizzle-orm';

import { db } from '@/lib/db';
import { ports } from '@/lib/db/schema/ports';
import { berths, berthMapData } from '@/lib/db/schema/berths';
import { interestBerths, interests } from '@/lib/db/schema/interests';
import { berthTenancies } from '@/lib/db/schema/tenancies';
import { errorResponse } from '@/lib/errors';
import { logger } from '@/lib/logger';
import { isTenanciesModuleEnabled } from '@/lib/services/tenancies-module.service';
import {
  isPermanentTenureType,
  toPublicBerth,
  type PublicBerth,
} from '@/lib/services/public-berths';

/**
 * GET /api/public/berths
 *
 * Public-website data feed. Returns the full berth list for the public-
 * facing port (default: port-nimara) in the same JSON shape NocoDB
 * returned, so the website's existing `getBerths()` swap is a one-line
 * URL change (plan §4.5 + §7.3).
 *
 * Auth: none. The endpoint is read-only and exposes only the explicit
 *   field allowlist defined in `toPublicBerth`.
 *
 * Caching: `s-maxage=300, stale-while-revalidate=60` matches the
 *   website's existing 5-minute TTL behaviour against NocoDB. Edge/CDN
 *   caches honour these headers; the Next.js fetch cache also picks
 *   them up.
 */

// Hard-coded allowlist for the public read-only feed. Adding a port here
// is a deliberate decision (not silent enumeration via ?portSlug=), so a
// future private tenant can't be exposed by accident.
const PUBLIC_PORT_SLUGS = new Set(['port-nimara']);
const DEFAULT_PUBLIC_PORT_SLUG = 'port-nimara';

const RESPONSE_HEADERS = {
  'cache-control': 'public, s-maxage=300, stale-while-revalidate=60',
  'content-type': 'application/json; charset=utf-8',
};

interface ListResponse {
  list: PublicBerth[];
  pageInfo: {
    totalRows: number;
    page: 1;
    pageSize: number;
    isFirstPage: true;
    isLastPage: true;
  };
}

export async function GET(request: Request): Promise<Response> {
  try {
    const url = new URL(request.url);
    const requestedSlug = url.searchParams.get('portSlug') ?? DEFAULT_PUBLIC_PORT_SLUG;
    if (!PUBLIC_PORT_SLUGS.has(requestedSlug)) {
      return NextResponse.json(
        { error: 'port is not part of the public berths feed', portSlug: requestedSlug },
        { status: 404, headers: { 'cache-control': 'no-store' } },
      );
    }
    const portSlug = requestedSlug;

    const [port] = await db
      .select({ id: ports.id })
      .from(ports)
      .where(eq(ports.slug, portSlug))
      .limit(1);
    if (!port) {
      return NextResponse.json(
        { error: 'port not found', portSlug },
        { status: 404, headers: { 'cache-control': 'no-store' } },
      );
    }

    // 1. Active berths for the port - retired moorings are hidden via
    //    the archived_at soft-delete column (migration 0065).
    const berthRows = await db
      .select()
      .from(berths)
      .where(and(eq(berths.portId, port.id), isNull(berths.archivedAt)));

    if (berthRows.length === 0) {
      return jsonResponse({ list: [], pageInfo: emptyPageInfo() });
    }

    const berthIds = berthRows.map((b) => b.id);

    // 2. Bulk-fetch map_data + the "has specific-interest link" flag.
    const [mapRows, specificInterestRows] = await Promise.all([
      db.select().from(berthMapData).where(inArray(berthMapData.berthId, berthIds)),
      db
        .selectDistinct({ berthId: interestBerths.berthId })
        .from(interestBerths)
        .innerJoin(interests, eq(interests.id, interestBerths.interestId))
        .where(
          and(
            inArray(interestBerths.berthId, berthIds),
            eq(interestBerths.isSpecificInterest, true),
            isNull(interests.archivedAt),
            // Don't promote a berth to "Under Offer" when the only specific-
            // interest link is a closed deal. `won` flips happen via
            // berths.status='sold' (handled in derivePublicStatus). Lost/
            // cancelled outcomes mean the berth is back on the market.
            isNull(interests.outcome),
          ),
        ),
    ]);

    const mapByBerth = new Map(mapRows.map((m) => [m.berthId, m]));
    const specificInterestSet = new Set(specificInterestRows.map((r) => r.berthId));

    // Tenancies module: per-port flag. When enabled, an active tenancy
    // with a permanent-class tenure type pushes a berth to "Sold" in the
    // public feed (per docs/tenancies-design.md §"Public map status flip").
    // When disabled, the lookup is skipped entirely — preserves the
    // pre-module behaviour for ports that haven't opted in.
    const permanentTenancyBerthIds = new Set<string>();
    if (await isTenanciesModuleEnabled(port.id)) {
      const activeTenancyRows = await db
        .select({ berthId: berthTenancies.berthId, tenureType: berthTenancies.tenureType })
        .from(berthTenancies)
        .where(
          and(
            eq(berthTenancies.portId, port.id),
            eq(berthTenancies.status, 'active'),
            inArray(berthTenancies.berthId, berthIds),
          ),
        );
      for (const row of activeTenancyRows) {
        if (isPermanentTenureType(row.tenureType)) permanentTenancyBerthIds.add(row.berthId);
      }
    }

    const list = berthRows.map((b) =>
      toPublicBerth(
        b,
        mapByBerth.get(b.id) ?? null,
        specificInterestSet.has(b.id),
        permanentTenancyBerthIds.has(b.id),
      ),
    );

    // Validate the response enum before returning - any unknown status
    // value would hit a 500 (per §14.8) rather than silently shipping
    // invalid data downstream.
    for (const row of list) {
      if (row.Status !== 'Available' && row.Status !== 'Under Offer' && row.Status !== 'Sold') {
        // Log just the identifying fields - never the full berth row, which
        // includes price + amenity columns that don't belong in error logs.
        logger.error(
          { berthId: row.Id, mooringNumber: row['Mooring Number'], status: row.Status },
          'Public berth status out of range',
        );
        throw new Error('Public berth status out of range');
      }
    }

    return jsonResponse({
      list,
      pageInfo: {
        totalRows: list.length,
        page: 1,
        pageSize: list.length,
        isFirstPage: true,
        isLastPage: true,
      },
    });
  } catch (err) {
    return errorResponse(err);
  }
}

function jsonResponse(body: ListResponse): Response {
  return new Response(JSON.stringify(body), { headers: RESPONSE_HEADERS, status: 200 });
}

function emptyPageInfo() {
  return {
    totalRows: 0,
    page: 1 as const,
    pageSize: 0,
    isFirstPage: true as const,
    isLastPage: true as const,
  };
}
