import { NextRequest, NextResponse } from 'next/server';
import { and, eq, isNull, sql } from 'drizzle-orm';
import type { z } from 'zod';

import { db } from '@/lib/db';
import { withTransaction } from '@/lib/db/utils';
import { interests, interestBerths } from '@/lib/db/schema/interests';
import { clients, clientContacts, clientAddresses } from '@/lib/db/schema/clients';
import { berths } from '@/lib/db/schema/berths';
import { ports } from '@/lib/db/schema/ports';
import { yachts, yachtOwnershipHistory } from '@/lib/db/schema/yachts';
import { companies, companyMemberships } from '@/lib/db/schema/companies';
import { createAuditLog } from '@/lib/audit';
import { parseBody } from '@/lib/api/route-helpers';
import { errorResponse, RateLimitError, ValidationError } from '@/lib/errors';
import { checkRateLimit, rateLimiters } from '@/lib/rate-limit';
import { publicInterestSchema } from '@/lib/validators/interests';
import { sendInquiryNotifications } from '@/lib/services/inquiry-notifications.service';
import { parsePhone } from '@/lib/i18n/phone';
import type { CountryCode } from '@/lib/i18n/countries';

/**
 * Throws RateLimitError if the IP has exceeded the public-form quota.
 * Backed by the Redis sliding-window limiter so the cap survives restarts
 * and is shared across worker processes.
 */
async function gateRateLimit(ip: string): Promise<void> {
  const result = await checkRateLimit(ip, rateLimiters.publicForm);
  if (!result.allowed) {
    const retryAfter = Math.max(1, Math.ceil((result.resetAt - Date.now()) / 1000));
    throw new RateLimitError(retryAfter);
  }
}

type PublicInterestData = z.infer<typeof publicInterestSchema>;
// `withTransaction` exposes its tx argument as `typeof db` (see lib/db/utils.ts).
// Keep the helper aligned with that.
type Tx = typeof db;

// POST /api/public/interests - unauthenticated public interest registration.
// Creates the trio (client + yacht + interest) plus an optional company +
// membership, all inside a single transaction.
export async function POST(req: NextRequest) {
  try {
    const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown';
    await gateRateLimit(ip);

    const data = await parseBody(req, publicInterestSchema);

    // Resolve portId from query param or header (public endpoints need explicit port)
    const portId = req.nextUrl.searchParams.get('portId') ?? req.headers.get('X-Port-Id');
    if (!portId) throw new ValidationError('Port context required');

    // Server-side phone normalization for older website builds that post raw
    // international/national strings. Newer builds may pre-fill phoneE164/Country.
    let phoneE164 = data.phoneE164 ?? null;
    let phoneCountry: CountryCode | null = (data.phoneCountry as CountryCode | null) ?? null;
    if (!phoneE164) {
      const parsed = parsePhone(data.phone, phoneCountry ?? undefined);
      phoneE164 = parsed.e164;
      phoneCountry = parsed.country ?? phoneCountry;
    }

    const fullName =
      data.firstName && data.lastName
        ? `${data.firstName} ${data.lastName}`
        : (data.fullName ?? 'Unknown');

    const firstName = data.firstName ?? fullName.split(/\s+/)[0] ?? 'Valued Guest';

    // Resolve berth by mooring number (if provided). Read-only lookup - safe
    // to do outside the transaction.
    let berthId: string | null = null;
    let resolvedMooringNumber: string | null = data.mooringNumber ?? null;
    if (data.mooringNumber) {
      const berth = await db.query.berths.findFirst({
        where: and(eq(berths.mooringNumber, data.mooringNumber), eq(berths.portId, portId)),
      });
      if (berth) {
        berthId = berth.id;
        resolvedMooringNumber = berth.mooringNumber;
      }
    }

    // ─── Transactional trio creation ────────────────────────────────────────
    const result = await withTransaction(async (tx) => {
      // 1. Find or create client by email. The inquiry-funnel audit
      //    flagged that the previous exact match was case-sensitive —
      //    capital-letter resubmissions spawned duplicate client+yacht+
      //    interest rows. Match LOWER(value) instead so foo@x.com and
      //    Foo@X.COM dedupe to the same client.
      let clientId: string;
      const normalizedEmail = data.email.trim().toLowerCase();
      const existingContact = await tx.query.clientContacts.findFirst({
        where: and(
          eq(clientContacts.channel, 'email'),
          sql`LOWER(${clientContacts.value}) = ${normalizedEmail}`,
        ),
      });
      if (existingContact) {
        const existingClient = await tx.query.clients.findFirst({
          where: eq(clients.id, existingContact.clientId),
        });
        if (existingClient && existingClient.portId === portId) {
          clientId = existingClient.id;
          const updates: Partial<typeof clients.$inferInsert> = {};
          if (data.preferredContactMethod) {
            updates.preferredContactMethod = data.preferredContactMethod;
          }
          if (data.nationalityIso && !existingClient.nationalityIso) {
            updates.nationalityIso = data.nationalityIso;
          }
          if (Object.keys(updates).length > 0) {
            await tx.update(clients).set(updates).where(eq(clients.id, clientId));
          }
        } else {
          clientId = await createClientInTx(tx, portId, fullName, data, phoneE164, phoneCountry);
        }
      } else {
        clientId = await createClientInTx(tx, portId, fullName, data, phoneE164, phoneCountry);
      }

      // 2. Optional: upsert company + add membership
      let companyId: string | null = null;
      if (data.company) {
        const existingCompany = await tx.query.companies.findFirst({
          where: and(
            eq(companies.portId, portId),
            sql`lower(${companies.name}) = lower(${data.company.name})`,
          ),
        });
        if (existingCompany) {
          companyId = existingCompany.id;
        } else {
          const [newCompany] = await tx
            .insert(companies)
            .values({
              portId,
              name: data.company.name,
              legalName: data.company.legalName ?? null,
              taxId: data.company.taxId ?? null,
              incorporationCountryIso: data.company.incorporationCountryIso ?? null,
              incorporationSubdivisionIso: data.company.incorporationSubdivisionIso ?? null,
              status: 'active',
            })
            .returning();
          companyId = newCompany!.id;
        }

        // Add active membership only if one doesn't already exist (open row).
        const existingMembership = await tx.query.companyMemberships.findFirst({
          where: and(
            eq(companyMemberships.companyId, companyId),
            eq(companyMemberships.clientId, clientId),
            isNull(companyMemberships.endDate),
          ),
        });
        if (!existingMembership) {
          await tx.insert(companyMemberships).values({
            companyId,
            clientId,
            role: data.company.role ?? 'representative',
            startDate: new Date(),
            isPrimary: false,
          });
        }
      }

      // 3. Create yacht. Owner is the company when provided, else the client.
      const ownerType: 'client' | 'company' = companyId ? 'company' : 'client';
      const ownerId = companyId ?? clientId;
      const [newYacht] = await tx
        .insert(yachts)
        .values({
          portId,
          name: data.yacht.name,
          hullNumber: data.yacht.hullNumber ?? null,
          registration: data.yacht.registration ?? null,
          flag: data.yacht.flag ?? null,
          yearBuilt: data.yacht.yearBuilt ?? null,
          lengthFt: data.yacht.lengthFt != null ? String(data.yacht.lengthFt) : null,
          widthFt: data.yacht.widthFt != null ? String(data.yacht.widthFt) : null,
          draftFt: data.yacht.draftFt != null ? String(data.yacht.draftFt) : null,
          currentOwnerType: ownerType,
          currentOwnerId: ownerId,
          status: 'active',
        })
        .returning();
      const yachtId = newYacht!.id;

      // 3a. Open ownership_history row for the new yacht.
      await tx.insert(yachtOwnershipHistory).values({
        yachtId,
        ownerType,
        ownerId,
        startDate: new Date(),
        endDate: null,
        createdBy: 'public-submission',
      });

      // 4. Store address if provided AND no primary address exists yet.
      if (data.address && Object.values(data.address).some(Boolean)) {
        const existingAddr = await tx.query.clientAddresses.findFirst({
          where: and(eq(clientAddresses.clientId, clientId), eq(clientAddresses.isPrimary, true)),
        });
        if (!existingAddr) {
          await tx.insert(clientAddresses).values({
            clientId,
            portId,
            label: 'Primary',
            streetAddress: data.address.street ?? null,
            city: data.address.city ?? null,
            subdivisionIso: data.address.subdivisionIso ?? null,
            postalCode: data.address.postalCode ?? null,
            countryIso: data.address.countryIso ?? null,
            isPrimary: true,
          });
        }
      }

      // 5. Create interest with yachtId wired up. The legacy
      // interests.berth_id column has been replaced by the
      // interest_berths junction (plan §3.4); when the public form
      // resolves to a known berth we materialise it as a primary,
      // specific-interest junction row in the same transaction so it
      // rolls back together with the parent interest insert.
      const [newInterest] = await tx
        .insert(interests)
        .values({
          portId,
          clientId,
          yachtId,
          source: 'website',
          pipelineStage: 'open',
        })
        .returning();

      if (berthId) {
        await tx.insert(interestBerths).values({
          interestId: newInterest!.id,
          berthId,
          isPrimary: true,
          isSpecificInterest: true,
          isInEoiBundle: false,
        });
      }

      return {
        interestId: newInterest!.id,
        clientId,
        yachtId,
        companyId,
      };
    });

    // ─── Post-commit side-effects (fire-and-forget) ─────────────────────────
    // `AuditLogParams.userId` is `string | null`; null is the documented
    // "system-generated" sentinel and matches `audit_logs.user_id` being
    // nullable in the schema. The earlier `null as unknown as string`
    // cast was a relic from before the type was widened.
    void createAuditLog({
      userId: null,
      portId,
      action: 'create',
      entityType: 'interest',
      entityId: result.interestId,
      newValue: {
        clientId: result.clientId,
        yachtId: result.yachtId,
        companyId: result.companyId,
        source: 'website',
        pipelineStage: 'open',
        berthId,
      },
      metadata: { type: 'public_registration', ip },
      ipAddress: ip,
      userAgent: req.headers.get('user-agent') ?? 'unknown',
    });

    const port = await db.query.ports.findFirst({
      where: eq(ports.id, portId),
      columns: { slug: true },
    });

    void sendInquiryNotifications({
      portId,
      portSlug: port?.slug ?? portId,
      interestId: result.interestId,
      clientFullName: fullName,
      clientEmail: data.email,
      clientPhone: data.phone,
      mooringNumber: resolvedMooringNumber,
      firstName,
    });

    return NextResponse.json(
      { data: { id: result.interestId, message: 'Interest registered successfully' } },
      { status: 201 },
    );
  } catch (error) {
    return errorResponse(error);
  }
}

// ─── Helpers ─────────────────────────────────────────────────────────────────

async function createClientInTx(
  tx: Tx,
  portId: string,
  fullName: string,
  data: Pick<PublicInterestData, 'email' | 'phone' | 'preferredContactMethod' | 'nationalityIso'>,
  phoneE164: string | null,
  phoneCountry: CountryCode | null,
): Promise<string> {
  const [newClient] = await tx
    .insert(clients)
    .values({
      portId,
      fullName,
      preferredContactMethod: data.preferredContactMethod,
      nationalityIso: data.nationalityIso ?? null,
      source: 'website',
    })
    .returning();
  const clientId = newClient!.id;

  await tx.insert(clientContacts).values({
    clientId,
    channel: 'email',
    // Store lowercased so the case-insensitive dedup match above always
    // hits on subsequent submissions.
    value: data.email.trim().toLowerCase(),
    isPrimary: true,
  });

  await tx.insert(clientContacts).values({
    clientId,
    channel: 'phone',
    value: data.phone,
    valueE164: phoneE164,
    valueCountry: phoneCountry,
    isPrimary: false,
  });

  return clientId;
}
