import { NextRequest, NextResponse } from "next/server";
import {
  buildVisionInput,
  encodeStoragePreview,
  processReceiptDocument,
} from "@/lib/image/processor";
import { extractReceiptData } from "@/lib/ai/extractor";
import { ProcessedReceipt } from "@/lib/schema/receipt";
import { getCurrentUser } from "@/lib/auth/session";
import { clientIp, rateLimit } from "@/lib/auth/rateLimit";
import { FREE_SCAN_LIMIT, LAUNCH_BONUS_SCANS, MAX_UPLOAD_BYTES } from "@/lib/limits";
import { guardBodySize, readFormDataSized } from "@/lib/http/requestSize";
import {
  checkDailyUsage,
  dailyScanLimitFor,
  recordUsage,
  usageKeyForUser,
} from "@/lib/ai/usage";
import { requireCsrf } from "@/lib/auth/csrf";
import { isProActive } from "@/lib/billing/access";
import { db, isDatabaseAvailable } from "@/lib/db";
import { ensureDatabaseInitialized } from "@/lib/db/init";
import { users, type User } from "@/lib/schema/db";
import { eq, sql } from "drizzle-orm";
import {
  RECEIPT_ALLOWED_KINDS,
  UnsupportedFileTypeError,
  assertAllowedUploadKind,
} from "@/lib/ingest/acceptedTypes";
import { sanitizeReceipt } from "@/lib/ingest/sanitize";
import { jsonForScanError } from "@/lib/http/scanErrors";

export const runtime = "nodejs";
export const maxDuration = 60; // 60 seconds timeout for AI generation

function currentMonthKey(date: Date = new Date()): string {
  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
}

function rateLimitedResponse(retryAfter: number) {
  return NextResponse.json(
    { error: "rate_limited", retryAfter },
    { status: 429, headers: { "Retry-After": String(retryAfter) } }
  );
}

export async function POST(req: NextRequest) {
  try {
    const csrfBlocked = requireCsrf(req);
    if (csrfBlocked) return csrfBlocked;

    // Reject oversized uploads from the Content-Length header BEFORE the
    // multipart body is buffered — a 2-GB upload must never reach formData().
    const tooLarge = guardBodySize(req, MAX_UPLOAD_BYTES);
    if (tooLarge) return tooLarge;

    const ipLimit = rateLimit(`scan:ip:${clientIp(req)}`, 60, 15 * 60 * 1000);
    if (!ipLimit.allowed) return rateLimitedResponse(ipLimit.retryAfter);

    let user: User | null = null;
    try {
      user = await getCurrentUser(req);
    } catch {
      user = null;
    }
    if (!user) {
      return NextResponse.json({ error: "unauthorized" }, { status: 401 });
    }
    if (!user.emailVerifiedAt) {
      return NextResponse.json({ error: "email_not_verified" }, { status: 403 });
    }

    const userLimit = rateLimit(`scan:user:${user.id}`, 120, 60 * 60 * 1000);
    if (!userLimit.allowed) return rateLimitedResponse(userLimit.retryAfter);

    // Daily AI-usage cap (in-memory) — Pro only. Free is limited by the
    // monthly DB quota (15, or 100 launch bonus). Unsigned callers never
    // reach this: dashboard and scan require a verified session.
    const usageKey = usageKeyForUser(user.id);
    const usageLimit = dailyScanLimitFor({ isPro: isProActive(user) });
    if (usageLimit !== null) {
      const daily = checkDailyUsage(usageKey, usageLimit);
      if (!daily.allowed) {
        const retryAfter = Math.ceil((daily.resetsAt - Date.now()) / 1000);
        return NextResponse.json(
          { error: "daily_scan_limit_reached", limit: usageLimit, retryAfter },
          { status: 429, headers: { "Retry-After": String(retryAfter) } }
        );
      }
    }

    // Streaming guard: a chunked request without a trustworthy Content-Length
    // must not let formData() buffer past MAX_UPLOAD_BYTES before file.size
    // is checked below.
    const formDataResult = await readFormDataSized(req, MAX_UPLOAD_BYTES);
    if (!formDataResult.ok) return formDataResult.response;
    const formData = formDataResult.formData;
    const file = formData.get("file") as File | null;

    if (!file) {
      return NextResponse.json({ error: "Keine Datei hochgeladen" }, { status: 400 });
    }
    if (file.size > MAX_UPLOAD_BYTES) {
      return NextResponse.json(
        { error: "Datei zu groß – maximal 10 MB erlaubt." },
        { status: 413 }
      );
    }

    const dbAvailable = await isDatabaseAvailable();
    if (dbAvailable) {
      await ensureDatabaseInitialized();

      if (!isProActive(user)) {
        const allowance = user.launchBonus ? LAUNCH_BONUS_SCANS : FREE_SCAN_LIMIT;
        const period = currentMonthKey();
        const reset = user.scanPeriod !== period;
        const used = reset ? 0 : Number(user.scanCount ?? 0);
        if (reset) {
          await db
            .update(users)
            .set({ scanCount: "0", scanPeriod: period, updatedAt: new Date() })
            .where(eq(users.id, user.id));
        }
        if (used >= allowance) {
          return NextResponse.json(
            { error: "scan_limit_reached", limit: allowance },
            { status: 402 }
          );
        }
      }
    }

    const arrayBuffer = await file.arrayBuffer();
    const inputBuffer = Buffer.from(arrayBuffer);
    const mimeType = file.type || "image/jpeg";
    const fileName = file.name || "receipt.jpg";

    // Whitelist-Gate VOR jeder Verarbeitung: Es entscheiden ausschließlich die
    // Magic Bytes des Buffers. Der deklarierte MIME-Type ist vom Client
    // steuerbar und gewährt nie Zugriff. Alles außer PDF/JPG/PNG/WebP/HEIC → 415.
    try {
      assertAllowedUploadKind(inputBuffer, mimeType, RECEIPT_ALLOWED_KINDS);
    } catch (error) {
      if (error instanceof UnsupportedFileTypeError) {
        return NextResponse.json({ error: error.message }, { status: 415 });
      }
      throw error;
    }

    // 1. Preprocessing & SHA-256 hashing. PDFs are rasterized to JPEG pages
    //    without archive encoding — preview is produced after extraction.
    const document = await processReceiptDocument(inputBuffer, mimeType, {
      encodePreview: false,
    });

    const visionInput = await buildVisionInput(document.pages);
    const extraction = await extractReceiptData({
      images: visionInput.images,
      imageMode: visionInput.mode,
      lineItemViews: visionInput.detailViews,
      forceLineItemVerification: visionInput.forceLineItemVerification,
      mimeType: "image/jpeg",
      fileName,
    });

    const previewSource = document.pages[0]?.buffer;
    const preview = previewSource
      ? await encodeStoragePreview(previewSource)
      : {
          previewUrl: undefined as string | undefined,
          processedSizeBytes: 0,
        };

    const now = new Date().toISOString();
    const receipts: ProcessedReceipt[] = [
      {
        ...extraction,
        id: `rcpt_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
        imageHash: document.sha256Hash,
        originalFileName: fileName,
        fileSizeBytes: preview.processedSizeBytes,
        previewUrl: preview.previewUrl,
        createdAt: now,
        updatedAt: now,
        status: extraction.validation.needsUserReview ? "needs_review" : "ready",
      } as ProcessedReceipt,
    ];

    if (receipts.length === 0) {
      return NextResponse.json(
        { error: "Aus dieser Datei konnte keine Seite gelesen werden." },
        { status: 422 }
      );
    }

    // Defense-in-depth: sanitize the extracted receipts (markup / control
    // chars / length caps — see src/lib/ingest/sanitize.ts) before they reach
    // the client, then re-uploaded to
    // POST /api/receipts (which applies the same validation again). The
    // extraction layer already constrains structure, so a failure here is
    // unexpected: log it and hand back the raw value rather than breaking the
    // scan UX.
    const safeReceipts = receipts.map((r) => {
      const safe = sanitizeReceipt(r);
      if (!safe) {
        console.warn(
          "[scan] extracted receipt failed storage sanitization; returning raw:",
          (r as { id?: unknown }).id ?? "(no id)"
        );
        return r;
      }
      return safe as unknown as ProcessedReceipt;
    });

    if (dbAvailable) {
      try {
        await db
          .update(users)
          .set({ scanCount: sql`${users.scanCount} + 1`, updatedAt: new Date() })
          .where(eq(users.id, user.id));
      } catch (counterError) {
        console.error("Scan counter increment failed:", counterError);
      }
    }

    // One merged document = one AI job (plus an optional in-extractor
    // reasoning retry that we do not double-count here).
    recordUsage(usageKey, 1);

    return NextResponse.json({
      success: true,
      // `receipt` stays for single-document callers; `receipts` carries every page.
      receipt: safeReceipts[0],
      receipts: safeReceipts,
      pageCount: document.pageCount,
      sourcePageCount: document.sourcePageCount,
      truncated: document.truncated,
    });
  } catch (error: any) {
    return jsonForScanError(error);
  }
}
