import { db } from "@/app/db/db";
import { NextRequest, NextResponse } from "next/server";
import { portfolioTag, propertyTag } from "@/app/db/schema/tags";
import { property } from "@/app/db/schema/property";
import { and, eq, inArray } from "drizzle-orm";
import { z } from "zod";
import { getServerSession } from "next-auth";
import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { canWriteTags, getPortfolioRole } from "@/lib/tags/server";
import { MAX_BULK_TAG_ROWS } from "@/lib/tags/bulkAssign";
import { getFilteredPropertyIds } from "@/app/portfolio/[slug]/utils";
import type { FilterGroups } from "@/app/utils/propertyFilters";

type Params = { params: Promise<{ portfolioId: string; tagId: string }> };

/** Batch size for membership writes — keeps a whole-portfolio op under the bind-param cap. */
const CHUNK = 5000;

const bodySchema = z.discriminatedUnion("mode", [
  z.object({
    mode: z.literal("properties"),
    action: z.enum(["add", "remove"]),
    propertyIds: z.array(z.string()).min(1),
  }),
  z.object({
    mode: z.literal("filter"),
    action: z.enum(["add", "remove"]),
    // The property-table filter groups; resolved to ids server-side so the whole
    // matching set is tagged, not just the visible page (ADR-0013).
    filterGroups: z.array(z.unknown()),
  }),
  z.object({
    mode: z.literal("identifiers"),
    key: z.enum(["landlord_property_id", "uprn"]),
    identifiers: z.array(z.string()).min(1).max(MAX_BULK_TAG_ROWS),
  }),
]);

/**
 * POST — add/remove Property memberships for one Tag (ADR-0013). `properties`
 * mode takes explicit ids (the inline popover + row multi-select); `identifiers`
 * mode matches a Bulk tag assignment upload's landlord ids / UPRNs to Properties
 * and returns a match summary. All writes are idempotent.
 */
export async function POST(req: NextRequest, props: Params) {
  const session = await getServerSession(AuthOptions);
  const { portfolioId, tagId } = await props.params;
  if (!session?.user?.email) {
    return NextResponse.json({ error: "Unauthorised" }, { status: 401 });
  }
  if (!/^\d+$/.test(portfolioId) || !/^\d+$/.test(tagId)) {
    return NextResponse.json({ error: "Invalid id" }, { status: 400 });
  }
  const pId = BigInt(portfolioId);
  const tId = BigInt(tagId);

  const role = await getPortfolioRole(pId, session.user.email);
  if (!canWriteTags(role)) {
    return NextResponse.json({ error: "Forbidden" }, { status: 403 });
  }

  // The tag must belong to this portfolio (prevents cross-portfolio writes).
  const tag = await db
    .select({ id: portfolioTag.id })
    .from(portfolioTag)
    .where(and(eq(portfolioTag.id, tId), eq(portfolioTag.portfolioId, pId)))
    .limit(1);
  if (tag.length === 0) {
    return NextResponse.json({ error: "Tag not found" }, { status: 404 });
  }

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

  if (body.mode === "properties" || body.mode === "filter") {
    let ids: bigint[];
    if (body.mode === "properties") {
      // Only ids that actually belong to this portfolio (guards spoofed ids).
      const owned = await db
        .select({ id: property.id })
        .from(property)
        .where(
          and(
            eq(property.portfolioId, pId),
            inArray(
              property.id,
              body.propertyIds.map((id) => BigInt(id)),
            ),
          ),
        );
      ids = owned.map((o) => o.id);
    } else {
      // Resolve the current property-table filter to its whole matching set.
      ids = await getFilteredPropertyIds(portfolioId, body.filterGroups as FilterGroups);
    }
    if (ids.length === 0) return NextResponse.json({ changed: 0 });

    // Chunk so a whole-portfolio selection stays under Postgres' bind-param cap.
    let changed = 0;
    for (let i = 0; i < ids.length; i += CHUNK) {
      const batch = ids.slice(i, i + CHUNK);
      if (body.action === "add") {
        const inserted = await db
          .insert(propertyTag)
          .values(batch.map((propertyId) => ({ propertyId, tagId: tId })))
          .onConflictDoNothing()
          .returning({ propertyId: propertyTag.propertyId });
        changed += inserted.length;
      } else {
        const removed = await db
          .delete(propertyTag)
          .where(
            and(eq(propertyTag.tagId, tId), inArray(propertyTag.propertyId, batch)),
          )
          .returning({ propertyId: propertyTag.propertyId });
        changed += removed.length;
      }
    }
    return NextResponse.json({ changed });
  }

  // identifiers mode — Bulk tag assignment (add only) with a match summary.
  if (body.mode !== "identifiers") {
    return NextResponse.json({ error: "Invalid body" }, { status: 400 });
  }
  // Capture the narrowed discriminant in a const — TS widens `let body` back to
  // the full union inside the closures below (e.g. matches.map).
  const key = body.key;
  const wanted = [...new Set(body.identifiers.map((s) => s.trim()).filter(Boolean))];
  const matches = await db
    .select({
      id: property.id,
      landlordPropertyId: property.landlordPropertyId,
      uprn: property.uprn,
    })
    .from(property)
    .where(
      and(
        eq(property.portfolioId, pId),
        key === "uprn"
          ? inArray(
              property.uprn,
              wanted
                .filter((v) => /^\d+$/.test(v))
                .map((v) => BigInt(v)),
            )
          : inArray(property.landlordPropertyId, wanted),
      ),
    );

  const matchedValues = new Set(
    matches.map((m) =>
      key === "uprn" ? String(m.uprn) : String(m.landlordPropertyId),
    ),
  );
  const unmatched = wanted.filter((v) => !matchedValues.has(v));
  const matchedPropertyIds = matches.map((m) => m.id);

  let taggedCount = 0;
  for (let i = 0; i < matchedPropertyIds.length; i += CHUNK) {
    const batch = matchedPropertyIds.slice(i, i + CHUNK);
    const inserted = await db
      .insert(propertyTag)
      .values(batch.map((propertyId) => ({ propertyId, tagId: tId })))
      .onConflictDoNothing()
      .returning({ propertyId: propertyTag.propertyId });
    taggedCount += inserted.length;
  }

  return NextResponse.json({
    matchedPropertyCount: matchedPropertyIds.length,
    taggedCount,
    alreadyTaggedCount: matchedPropertyIds.length - taggedCount,
    unmatchedCount: unmatched.length,
    unmatchedIdentifiers: unmatched.slice(0, 100),
  });
}
