import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { z } from "zod";
import { db } from "@/app/db/db";
import { property, propertyDetailsSpatial } from "@/app/db/schema/property";
import {
  landlordBuiltFormTypeOverrides,
  landlordPropertyTypeOverrides,
} from "@/app/db/schema/landlord_overrides";
import { propertyOverrides } from "@/app/db/schema/property_overrides";
import { and, eq, inArray } from "drizzle-orm";
import {
  buildWritePlan,
  classifyOsCode,
  normalisePostcode,
} from "@/lib/postcodeSearch/model";

const bodySchema = z.object({
  geo: z.object({
    localAuthority: z.string().nullable(),
    constituency: z.string().nullable(),
  }),
  candidates: z
    .array(
      z.object({
        uprn: z.string().regex(/^\d+$/),
        address: z.string().trim().min(1),
        postcode: z.string(),
        classificationCode: z.string().nullable(),
        lat: z.number().nullable(),
        lng: z.number().nullable(),
      }),
    )
    .min(1)
    .max(500),
});

// POST — create the selected address candidates as Properties (ADR-0007):
// property rows via the uq_property_portfolio_uprn dedup, plus both override
// layers (vocabulary upsert + per-property snapshot) with os_places provenance.
// Facts are re-derived server-side from the classification code — the client's
// propertyType/builtForm are never trusted.
export async function POST(
  request: NextRequest,
  props: { params: Promise<{ portfolioId: string }> },
) {
  const session = await getServerSession(AuthOptions);
  if (!session?.user?.email) {
    return NextResponse.json({ error: "Unauthorised" }, { status: 401 });
  }

  const { portfolioId } = await props.params;
  if (!/^\d+$/.test(portfolioId)) {
    return NextResponse.json({ error: "Invalid portfolio id" }, { status: 400 });
  }
  const pid = BigInt(portfolioId);

  let body: z.infer<typeof bodySchema>;
  try {
    body = bodySchema.parse(await request.json());
  } catch {
    return NextResponse.json({ error: "Invalid body" }, { status: 400 });
  }

  const seen = new Set<string>();
  const plans: ReturnType<typeof buildWritePlan>[] = [];
  for (const c of body.candidates) {
    const postcode = normalisePostcode(c.postcode);
    if (!postcode) {
      return NextResponse.json(
        { error: `Invalid postcode '${c.postcode}'` },
        { status: 400 },
      );
    }
    const classificationCode = c.classificationCode?.trim().toUpperCase() ?? null;
    const cls = classifyOsCode(classificationCode);
    if (!cls.selectable || seen.has(c.uprn)) continue;
    seen.add(c.uprn);
    plans.push(
      buildWritePlan(
        { ...c, postcode, classificationCode, ...cls },
        body.geo,
      ),
    );
  }
  if (plans.length === 0) {
    return NextResponse.json(
      { error: "No addable addresses in the selection" },
      { status: 400 },
    );
  }

  try {
    const result = await db.transaction(async (tx) => {
      // Vocabulary layer: one upsert per distinct OS code, then read back the
      // resolved value so snapshots mirror it — DO NOTHING preserves any
      // existing user/classifier row for the same description (existing rule).
      const factsFor = (component: string) => {
        const byDescription = new Map<string, string>();
        for (const p of plans)
          for (const o of p.overrides)
            if (o.component === component)
              byDescription.set(o.originalDescription, o.value);
        return byDescription;
      };
      const resolved = new Map<string, string>();
      for (const [component, table] of [
        ["property_type", landlordPropertyTypeOverrides],
        ["built_form_type", landlordBuiltFormTypeOverrides],
      ] as const) {
        const facts = factsFor(component);
        if (facts.size === 0) continue;
        await tx
          .insert(table)
          .values(
            [...facts].map(([description, value]) => ({
              portfolioId: pid,
              description,
              value,
              source: "os_places",
            })),
          )
          .onConflictDoNothing();
        const rows = await tx
          .select({ description: table.description, value: table.value })
          .from(table)
          .where(
            and(
              eq(table.portfolioId, pid),
              inArray(table.description, [...facts.keys()]),
            ),
          );
        for (const r of rows) resolved.set(`${component}|${r.description}`, r.value);
      }

      const inserted = await tx
        .insert(property)
        .values(
          plans.map((p) => ({
            portfolioId: pid,
            creationStatus: "READY",
            uprn: p.property.uprn,
            address: p.property.address,
            postcode: p.property.postcode,
            localAuthority: p.property.localAuthority,
            constituency: p.property.constituency,
          })),
        )
        .onConflictDoNothing()
        .returning({ id: property.id, uprn: property.uprn });
      const idByUprn = new Map(
        inserted.map((r) => [r.uprn!.toString(), r.id]),
      );

      const spatialRows = plans
        .filter(
          (p) =>
            idByUprn.has(p.property.uprn.toString()) &&
            p.property.latitude != null &&
            p.property.longitude != null,
        )
        .map((p) => ({
          uprn: p.property.uprn,
          latitude: p.property.latitude,
          longitude: p.property.longitude,
        }));
      if (spatialRows.length > 0) {
        await tx.insert(propertyDetailsSpatial).values(spatialRows).onConflictDoNothing();
      }

      const snapshotRows = plans.flatMap((p) => {
        const propertyId = idByUprn.get(p.property.uprn.toString());
        if (!propertyId) return [];
        return p.overrides.map((o) => ({
          propertyId,
          portfolioId: pid,
          buildingPart: o.buildingPart,
          overrideComponent: o.component,
          overrideValue:
            resolved.get(`${o.component}|${o.originalDescription}`) ?? o.value,
          originalSpreadsheetDescription: o.originalDescription,
        }));
      });
      if (snapshotRows.length > 0) {
        await tx.insert(propertyOverrides).values(snapshotRows).onConflictDoNothing();
      }

      return { added: inserted.length, skipped: plans.length - inserted.length };
    });

    return NextResponse.json(result);
  } catch (err) {
    console.error("POST /properties error:", err);
    return NextResponse.json({ error: "Internal server error" }, { status: 500 });
  }
}
