import { NextRequest, NextResponse } from "next/server";
import { db, isDatabaseAvailable } from "@/lib/db";
import { ensureDatabaseInitialized } from "@/lib/db/init";
import { receipts, users, projects } from "@/lib/schema/db";
import { ProcessedReceipt } from "@/lib/schema/receipt";
import { eq, and, desc } from "drizzle-orm";
import { sanitizeReceiptBatch } from "@/lib/ingest/sanitize";
import { getCurrentUser } from "@/lib/auth/session";
import { isAdmin } from "@/lib/auth/admin";
import { isProActive } from "@/lib/billing/access";
import { MAX_RECEIPTS_JSON_BYTES } from "@/lib/limits";
import { readJsonSized } from "@/lib/http/requestSize";
import { dbRowToProcessedReceipt, toExtractionJson } from "@/lib/storage/receiptRow";
import { requireCsrf } from "@/lib/auth/csrf";
import {
  resolveGuestContext,
  applyGuestCookie,
  type GuestContext,
} from "@/lib/auth/guest";

export const runtime = "nodejs";

/**
 * Resolves the bucket this request is allowed to touch. Signed-in users are
 * always scoped to their own account; signed-out callers fall back to their
 * per-browser guest bucket. Client-supplied ids are never trusted here.
 */
async function resolveScope(req: NextRequest): Promise<{
  user: Awaited<ReturnType<typeof getCurrentUser>>;
  userId: string;
  guest: GuestContext | null;
}> {
  const user = await getCurrentUser(req);
  if (user) return { user, userId: user.id, guest: null };

  const guest = resolveGuestContext(req);
  return { user: null, userId: guest.bucket, guest };
}

/**
 * GET /api/receipts
 * List (default) omits preview data-URLs. Pass `includePreview=1` for a
 * single receipt (`id=`) or a full list with images.
 */
export async function GET(req: NextRequest) {
  try {
    const { searchParams } = new URL(req.url);
    const limitParam = searchParams.get("limit");
    const limit = Math.min(Math.max(parseInt(limitParam ?? "100", 10) || 100, 1), 500);
    const includePreview = searchParams.get("includePreview") === "1";
    const receiptId = searchParams.get("id");

    const dbAvailable = await isDatabaseAvailable();
    if (!dbAvailable) {
      return NextResponse.json(
        {
          success: false,
          message: "Database is unavailable.",
          receipts: [],
          count: 0,
        },
        { status: 503 }
      );
    }

    await ensureDatabaseInitialized();

    const scope = await resolveScope(req);
    let userId = scope.userId;
    if (scope.user && isAdmin(scope.user.email)) {
      const requested = searchParams.get("userId");
      if (requested) userId = requested;
    }

    if (receiptId) {
      const rows = await db
        .select()
        .from(receipts)
        .where(and(eq(receipts.id, receiptId), eq(receipts.userId, userId)))
        .limit(1);
      if (rows.length === 0) {
        return applyGuestCookie(
          NextResponse.json({ success: false, error: "not_found" }, { status: 404 }),
          scope.guest
        );
      }
      const receipt = dbRowToProcessedReceipt(rows[0], { includePreview: true });
      return applyGuestCookie(
        NextResponse.json({ success: true, receipt, receipts: [receipt], count: 1 }),
        scope.guest
      );
    }

    const rows = await db
      .select()
      .from(receipts)
      .where(eq(receipts.userId, userId))
      .orderBy(desc(receipts.createdAt))
      .limit(limit);

    const processedReceipts: ProcessedReceipt[] = rows.map((row) =>
      dbRowToProcessedReceipt(row, { includePreview })
    );

    return applyGuestCookie(
      NextResponse.json({
        success: true,
        receipts: processedReceipts,
        count: processedReceipts.length,
      }),
      scope.guest
    );
  } catch (error: any) {
    console.error("GET /api/receipts error:", error);
    return NextResponse.json(
      {
        success: false,
        message: "Failed to retrieve receipts from database.",
        receipts: [],
      },
      { status: 500 }
    );
  }
}

/**
 * POST /api/receipts
 * Batch upserts receipts and relational line items into PostgreSQL. Receipts are
 * always attributed to the authenticated user (or guest bucket) — a userId from
 * the payload is never honoured.
 * If PostgreSQL is unavailable, responds with HTTP 503.
 */
export async function POST(req: NextRequest) {
  try {
    const csrfBlocked = requireCsrf(req);
    if (csrfBlocked) return csrfBlocked;

    const dbAvailable = await isDatabaseAvailable();
    if (!dbAvailable) {
      return NextResponse.json(
        {
          success: false,
          message: "Database is unavailable.",
        },
        { status: 503 }
      );
    }

    const parsed = await readJsonSized<unknown>(req, MAX_RECEIPTS_JSON_BYTES);
    if (!parsed.ok) return parsed.response;

    // Security boundary: every client-supplied receipt is validated (structure
    // → 400 with a clear error) and sanitized (markup/control chars/lengths)
    // BEFORE anything touches the database. See src/lib/ingest/sanitize.ts for
    // the exact policy. A single invalid receipt rejects the whole batch —
    // silently dropping user data would be worse than asking the client to fix
    // the payload.
    const batch = sanitizeReceiptBatch(parsed.body);
    if (!batch.ok || !batch.receipts) {
      return NextResponse.json(
        { error: batch.error ?? "Invalid receipt payload." },
        { status: 400 }
      );
    }
    const receiptsList = batch.receipts;

    await ensureDatabaseInitialized();

    const scope = await resolveScope(req);
    const targetUserId = scope.userId;

    // Guest buckets are server-minted ids in the `guest_*` namespace, so this
    // upsert can only ever materialise rows for the guest's own bucket — never
    // for an arbitrary id supplied by the client.
    if (!scope.user) {
      await db
        .insert(users)
        .values({
          id: targetUserId,
          isGuest: true,
          plan: "free",
        })
        .onConflictDoNothing();
    }

    // Assigning a receipt to a project at upload time is Pro-only. Validated
    // project ids are cached per-request since a batch typically targets the
    // same folder for every file.
    const isPro = isProActive(scope.user);
    const validatedProjectIds = new Map<string, boolean>();
    async function resolveProjectId(candidate: string | null | undefined): Promise<string | null> {
      if (!candidate || !isPro) return null;
      const cached = validatedProjectIds.get(candidate);
      if (cached !== undefined) return cached ? candidate : null;
      const owned = await db
        .select({ id: projects.id })
        .from(projects)
        .where(and(eq(projects.id, candidate), eq(projects.userId, targetUserId)))
        .limit(1);
      const ok = owned.length > 0;
      validatedProjectIds.set(candidate, ok);
      return ok ? candidate : null;
    }

    // Process and upsert each receipt
    for (const receipt of receiptsList) {
      // Never mutate a row that already belongs to another account.
      const existing = await db
        .select({ ownerId: receipts.userId })
        .from(receipts)
        .where(eq(receipts.id, receipt.id))
        .limit(1);
      if (existing.length > 0 && existing[0].ownerId !== targetUserId) {
        continue;
      }

      let tax7: number | null = null;
      let tax19: number | null = null;

      if (Array.isArray(receipt.taxBreakdown)) {
        for (const item of receipt.taxBreakdown) {
          if (item.ratePercent === 7) tax7 = (tax7 ?? 0) + item.taxAmount;
          if (item.ratePercent === 19) tax19 = (tax19 ?? 0) + item.taxAmount;
        }
      }

      const isMathValid = receipt.validation?.isMathValid ?? true;
      const needsReview =
        receipt.validation?.needsUserReview ?? (receipt.status === "needs_review");

      // If projectId is explicitly provided in the payload, validate and update it
      const shouldUpdateProjectId = receipt.projectId !== undefined;
      const projectIdResolved = shouldUpdateProjectId
        ? await resolveProjectId(receipt.projectId)
        : null;

      // Upsert receipt row
      await db
        .insert(receipts)
        .values({
          id: receipt.id,
          userId: targetUserId,
          imageHash: receipt.imageHash || `hash_${receipt.id}`,
          projectId: projectIdResolved,
          storageUrl: receipt.previewUrl || null,
          merchantName: receipt.merchant?.name || null,
          receiptDate: receipt.date?.isoDate || null,
          receiptNumber: receipt.receiptNumber || null,
          documentType: receipt.documentType || "KASSENBON",
          category: receipt.suggestedCategory || "Sonstiges",
          currency: receipt.currency || "EUR",
          totalAmount: String(receipt.totalAmount?.value ?? 0),
          netAmount: receipt.netAmount != null ? String(receipt.netAmount) : null,
          tax7Amount: tax7 != null ? String(tax7) : null,
          tax19Amount: tax19 != null ? String(tax19) : null,
          tipAmount: receipt.tipAmount != null ? String(receipt.tipAmount) : null,
          taxBreakdownJson: receipt.taxBreakdown || [],
          lineItemsJson: receipt.lineItems || [],
          validationJson: receipt.validation || null,
          extractionJson: toExtractionJson(receipt as ProcessedReceipt),
          rawOcrText: receipt.rawText || null,
          paymentMethod: receipt.paymentMethod ? String(receipt.paymentMethod) : null,
          isMathValid,
          needsReview,
          createdAt: receipt.createdAt ? new Date(receipt.createdAt) : new Date(),
          updatedAt: new Date(),
        })
        .onConflictDoUpdate({
          target: receipts.id,
          set: {
            userId: targetUserId,
            ...(shouldUpdateProjectId ? { projectId: projectIdResolved } : {}),
            merchantName: receipt.merchant?.name || null,
            receiptDate: receipt.date?.isoDate || null,
            receiptNumber: receipt.receiptNumber || null,
            documentType: receipt.documentType || "KASSENBON",
            category: receipt.suggestedCategory || "Sonstiges",
            currency: receipt.currency || "EUR",
            totalAmount: String(receipt.totalAmount?.value ?? 0),
            netAmount: receipt.netAmount != null ? String(receipt.netAmount) : null,
            tax7Amount: tax7 != null ? String(tax7) : null,
            tax19Amount: tax19 != null ? String(tax19) : null,
            tipAmount: receipt.tipAmount != null ? String(receipt.tipAmount) : null,
            taxBreakdownJson: receipt.taxBreakdown || [],
            lineItemsJson: receipt.lineItems || [],
            validationJson: receipt.validation || null,
            extractionJson: toExtractionJson(receipt as ProcessedReceipt),
            rawOcrText: receipt.rawText || null,
            paymentMethod: receipt.paymentMethod ? String(receipt.paymentMethod) : null,
            isMathValid,
            needsReview,
            updatedAt: new Date(),
            ...(receipt.previewUrl ? { storageUrl: receipt.previewUrl } : {}),
            ...(receipt.imageHash ? { imageHash: receipt.imageHash } : {}),
          },
        });

      // Line items live in `line_items_json` / `extraction_json` only.
      // The relational `line_items` table is not written (avoids dual storage).
    }

    return applyGuestCookie(
      NextResponse.json({
        success: true,
        syncedCount: receiptsList.length,
        message: `Successfully synchronized ${receiptsList.length} receipt(s) with PostgreSQL.`,
      }),
      scope.guest
    );
  } catch (error: any) {
    console.error("POST /api/receipts error:", error);
    return NextResponse.json(
      {
        success: false,
        error: "Failed to persist receipts to database.",
      },
      { status: 500 }
    );
  }
}

/**
 * DELETE /api/receipts
 * Deletes a receipt by ID from PostgreSQL — but only if it belongs to the
 * authenticated user (or guest bucket).
 */
export async function DELETE(req: NextRequest) {
  try {
    const csrfBlocked = requireCsrf(req);
    if (csrfBlocked) return csrfBlocked;

    const { searchParams } = new URL(req.url);
    const id = searchParams.get("id");

    if (!id) {
      return NextResponse.json({ error: "Missing receipt id." }, { status: 400 });
    }

    const dbAvailable = await isDatabaseAvailable();
    if (!dbAvailable) {
      return NextResponse.json(
        {
          success: false,
          message: "Database is unavailable.",
        },
        { status: 503 }
      );
    }

    await ensureDatabaseInitialized();

    const scope = await resolveScope(req);

    const existing = await db
      .select({ id: receipts.id })
      .from(receipts)
      .where(and(eq(receipts.id, id), eq(receipts.userId, scope.userId)))
      .limit(1);

    if (existing.length === 0) {
      return applyGuestCookie(
        NextResponse.json(
          { error: "Receipt not found or not owned by the current account." },
          { status: 404 }
        ),
        scope.guest
      );
    }

    await db.delete(receipts).where(eq(receipts.id, id));

    return applyGuestCookie(
      NextResponse.json({
        success: true,
        message: `Receipt ${id} deleted successfully from PostgreSQL.`,
      }),
      scope.guest
    );
  } catch (error: any) {
    console.error("DELETE /api/receipts error:", error);
    return NextResponse.json(
      { error: "Failed to delete receipt." },
      { status: 500 }
    );
  }
}