/**
 * Registratura API — Main CRUD endpoints.
 *
 * POST — Create entry (atomic numbering, late registration, audit)
 * GET  — List entries / get single entry
 * PUT  — Update entry (diff audit)
 * DELETE — Delete entry (audit)
 *
 * Auth: NextAuth session OR Bearer API key (REGISTRY_API_KEY env).
 */

import { NextRequest, NextResponse } from "next/server";
import type { Prisma } from "@prisma/client";
import { v4 as uuid } from "uuid";
import { prisma } from "@/core/storage/prisma";
import { getAuthSession } from "@/core/auth";
import { allocateSequenceNumber, recalculateSequence, parseRegistryNumber } from "@/modules/registratura/services/registry-service";
import { REGISTRY_COMPANY_PREFIX, OLD_COMPANY_PREFIX } from "@/modules/registratura/types";
import {
  logAuditEvent,
  computeEntryDiff,
  TRACKED_FIELDS,
} from "@/modules/registratura/services/audit-service";
import { findAvailableReservedSlot } from "@/modules/registratura/services/reserved-slots-service";
import type { RegistryEntry, RegistryDirection } from "@/modules/registratura/types";
import type { CompanyId } from "@/core/auth/types";

const NAMESPACE = "registratura";
const BLOB_NAMESPACE = "registratura-blobs";
const STORAGE_PREFIX = "entry:";

// ── Auth ──

interface Actor {
  id: string;
  name: string;
  company?: string;
}

async function authenticateRequest(req: NextRequest): Promise<Actor | null> {
  // 1. Check NextAuth session
  const session = await getAuthSession();
  if (session?.user) {
    const u = session.user as { id?: string; name?: string | null; email?: string | null; company?: string };
    return {
      id: u.id ?? u.email ?? "unknown",
      name: u.name ?? u.email ?? "unknown",
      company: u.company,
    };
  }

  // 2. Check API key
  const apiKey = process.env.REGISTRY_API_KEY;
  if (apiKey) {
    const auth = req.headers.get("authorization");
    if (auth === `Bearer ${apiKey}`) {
      return { id: "api-key", name: "ERP Integration" };
    }
  }

  return null;
}

// ── Helpers ──

/** Strip base64 data >1KB from entry JSON (same logic as /api/storage lightweight) */
function stripHeavyFields(obj: unknown): unknown {
  if (typeof obj === "string") return obj.length > 1024 ? "__stripped__" : obj;
  if (Array.isArray(obj)) return obj.map(stripHeavyFields);
  if (obj && typeof obj === "object") {
    const result: Record<string, unknown> = {};
    for (const [k, v] of Object.entries(obj)) {
      if ((k === "data" || k === "fileData") && typeof v === "string" && v.length > 1024) {
        result[k] = "__stripped__";
      } else {
        result[k] = stripHeavyFields(v);
      }
    }
    return result;
  }
  return obj;
}

async function loadAllEntries(lightweight = true): Promise<RegistryEntry[]> {
  const rows = await prisma.keyValueStore.findMany({
    where: { namespace: NAMESPACE },
    select: { key: true, value: true },
  });

  const entries: RegistryEntry[] = [];
  for (const row of rows) {
    if (row.key.startsWith(STORAGE_PREFIX) && row.value) {
      const val = lightweight ? stripHeavyFields(row.value) : row.value;
      entries.push(val as unknown as RegistryEntry);
    }
  }
  entries.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
  return entries;
}

async function loadEntry(id: string): Promise<RegistryEntry | null> {
  const row = await prisma.keyValueStore.findUnique({
    where: { namespace_key: { namespace: NAMESPACE, key: `${STORAGE_PREFIX}${id}` } },
  });
  return (row?.value as unknown as RegistryEntry) ?? null;
}

async function saveEntryToDB(entry: RegistryEntry): Promise<void> {
  await prisma.keyValueStore.upsert({
    where: { namespace_key: { namespace: NAMESPACE, key: `${STORAGE_PREFIX}${entry.id}` } },
    update: { value: entry as unknown as Prisma.InputJsonValue },
    create: {
      namespace: NAMESPACE,
      key: `${STORAGE_PREFIX}${entry.id}`,
      value: entry as unknown as Prisma.InputJsonValue,
    },
  });
}

async function deleteEntryFromDB(id: string): Promise<void> {
  await prisma.keyValueStore.deleteMany({
    where: { namespace: NAMESPACE, key: `${STORAGE_PREFIX}${id}` },
  });
  // Clean up blobs
  await prisma.keyValueStore.deleteMany({
    where: { namespace: BLOB_NAMESPACE, key: id },
  });
}

// ── GET — List or get single entry ──

export async function GET(req: NextRequest) {
  const actor = await authenticateRequest(req);
  if (!actor) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const url = new URL(req.url);
  const id = url.searchParams.get("id");
  const company = url.searchParams.get("company");
  const year = url.searchParams.get("year");
  const type = url.searchParams.get("type");
  const status = url.searchParams.get("status");
  const full = url.searchParams.get("full") === "true";

  // Single entry
  if (id) {
    const entry = await loadEntry(id);
    if (!entry) {
      return NextResponse.json({ error: "Not found" }, { status: 404 });
    }
    return NextResponse.json({ success: true, entry });
  }

  // List with optional filters
  let entries = await loadAllEntries(!full);

  if (company) entries = entries.filter((e) => e.company === company);
  if (year) {
    const yr = parseInt(year, 10);
    entries = entries.filter((e) => {
      const d = new Date(e.date);
      return d.getFullYear() === yr;
    });
  }
  if (type) {
    const dirMap: Record<string, RegistryDirection> = {
      IN: "intrat",
      OUT: "iesit",
    };
    const dir = dirMap[type.toUpperCase()];
    if (dir) entries = entries.filter((e) => e.direction === dir);
  }
  if (status) entries = entries.filter((e) => e.status === status);

  return NextResponse.json({ success: true, entries, total: entries.length });
}

// ── POST — Create entry with atomic numbering ──

export async function POST(req: NextRequest) {
  const actor = await authenticateRequest(req);
  if (!actor) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  try {
    const body = await req.json();
    const entryData = body.entry as Partial<RegistryEntry>;

    if (!entryData.company || !entryData.direction) {
      return NextResponse.json(
        { error: "Missing required fields: company, direction" },
        { status: 400 },
      );
    }

    const company = entryData.company as CompanyId;
    const direction = entryData.direction as RegistryDirection;
    const now = new Date().toISOString();
    const entryDate = entryData.date ?? now.slice(0, 10);

    // Check if this is a late registration (document date is in a past month)
    const docDate = new Date(entryDate);
    const today = new Date();
    const isPastMonth =
      docDate.getFullYear() < today.getFullYear() ||
      (docDate.getFullYear() === today.getFullYear() &&
        docDate.getMonth() < today.getMonth());

    let registryNumber: string;
    let registrationType: "normal" | "late" | "reserved-claimed" = "normal";
    let claimedSlotId: string | undefined;

    if (isPastMonth && direction === "intrat") {
      // Try to claim a reserved slot — use advisory lock to prevent concurrent claims
      const lockKey = `reserved:${company}-${docDate.getFullYear()}-${docDate.getMonth()}`;
      const claimed = await prisma.$transaction(async (tx) => {
        await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtext(${lockKey}))`;
        const allEntries = await loadAllEntries(true);
        const slot = findAvailableReservedSlot(
          allEntries,
          company,
          docDate.getFullYear(),
          docDate.getMonth(),
        );
        if (!slot) return null;
        // Delete the placeholder slot within the lock
        await tx.keyValueStore.delete({
          where: { namespace_key: { namespace: "registratura", key: slot.id } },
        });
        return slot;
      });

      if (claimed) {
        registryNumber = claimed.number;
        registrationType = "reserved-claimed";
        claimedSlotId = claimed.id;

        await logAuditEvent({
          entryId: claimed.id,
          entryNumber: claimed.number,
          action: "reserved_claimed",
          actor: actor.id,
          actorName: actor.name,
          company: company,
          detail: { claimedBy: entryData.subject ?? "late registration" },
        });
      } else {
        // No reserved slot — allocate new number, mark as late
        const { number } = await allocateSequenceNumber(company, direction);
        registryNumber = number;
        registrationType = "late";
      }
    } else if (isPastMonth) {
      // OUT/INT late registration — always get new number
      const { number } = await allocateSequenceNumber(company, direction);
      registryNumber = number;
      registrationType = "late";
    } else {
      // Normal registration
      const { number } = await allocateSequenceNumber(company, direction);
      registryNumber = number;
    }

    const entry: RegistryEntry = {
      id: entryData.id ?? uuid(),
      number: registryNumber,
      date: entryDate,
      registrationDate: entryData.registrationDate ?? now.slice(0, 10),
      direction,
      documentType: entryData.documentType ?? "altele",
      subject: entryData.subject ?? "",
      sender: entryData.sender ?? "",
      senderContactId: entryData.senderContactId,
      recipient: entryData.recipient ?? "",
      recipientContactId: entryData.recipientContactId,
      recipientRegNumber: entryData.recipientRegNumber,
      recipientRegDate: entryData.recipientRegDate,
      company,
      status: entryData.status ?? "deschis",
      closureInfo: entryData.closureInfo,
      deadline: entryData.deadline,
      assignee: entryData.assignee,
      assigneeContactId: entryData.assigneeContactId,
      threadParentId: entryData.threadParentId,
      linkedEntryIds: entryData.linkedEntryIds ?? [],
      attachments: entryData.attachments ?? [],
      trackedDeadlines: entryData.trackedDeadlines,
      expiryDate: entryData.expiryDate,
      expiryAlertDays: entryData.expiryAlertDays,
      externalStatusUrl: entryData.externalStatusUrl,
      externalTrackingId: entryData.externalTrackingId,
      acValidity: entryData.acValidity,
      tags: entryData.tags ?? [],
      notes: entryData.notes ?? "",
      visibility: entryData.visibility ?? "internal",
      createdAt: now,
      updatedAt: now,
      isReserved: false,
      registrationType,
      createdBy: actor.id,
      createdByName: actor.name,
      claimedReservedSlotId: claimedSlotId,
    };

    await saveEntryToDB(entry);

    // Audit: created
    await logAuditEvent({
      entryId: entry.id,
      entryNumber: entry.number,
      action: registrationType === "late" ? "late_registration" : "created",
      actor: actor.id,
      actorName: actor.name,
      company: company,
      detail: {
        direction,
        registrationType,
        ...(claimedSlotId ? { claimedSlotId } : {}),
      },
    });

    return NextResponse.json({ success: true, entry }, { status: 201 });
  } catch (err) {
    const message = err instanceof Error ? err.message : "Internal error";
    return NextResponse.json({ error: message }, { status: 500 });
  }
}

// ── PUT — Update entry ──

export async function PUT(req: NextRequest) {
  const actor = await authenticateRequest(req);
  if (!actor) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  try {
    const body = await req.json();
    const { id, updates } = body as { id: string; updates: Partial<RegistryEntry> };

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

    const existing = await loadEntry(id);
    if (!existing) {
      return NextResponse.json({ error: "Not found" }, { status: 404 });
    }

    // Prevent changing immutable fields
    delete updates.id;
    delete updates.number;
    delete updates.createdAt;
    delete updates.createdBy;
    delete updates.createdByName;

    // Re-allocate registry number when company or direction changes
    const companyChanged = updates.company && updates.company !== existing.company;
    const directionChanged = updates.direction && updates.direction !== existing.direction;
    let newNumber: string | undefined;
    if (companyChanged || directionChanged) {
      const targetCompany = (updates.company ?? existing.company) as CompanyId;
      const targetDirection = (updates.direction ?? existing.direction) as RegistryDirection;
      const allocated = await allocateSequenceNumber(targetCompany, targetDirection);
      newNumber = allocated.number;
    }

    const updated: RegistryEntry = {
      ...existing,
      ...updates,
      ...(newNumber ? { number: newNumber } : {}),
      updatedAt: new Date().toISOString(),
    };

    // Compute diff for audit
    const diff = computeEntryDiff(
      existing as unknown as Record<string, unknown>,
      updated as unknown as Record<string, unknown>,
      TRACKED_FIELDS,
    );

    await saveEntryToDB(updated);

    // Recalculate old company's counter so next entry gets the correct number
    if (companyChanged || directionChanged) {
      await recalculateSequence(
        existing.company as CompanyId,
        existing.direction as RegistryDirection,
      );
    }

    if (diff) {
      const action = updates.status === "inchis" && existing.status !== "inchis"
        ? "closed" as const
        : updates.status === "deschis" && existing.status === "inchis"
          ? "reopened" as const
          : "updated" as const;

      await logAuditEvent({
        entryId: id,
        entryNumber: updated.number,
        action,
        actor: actor.id,
        actorName: actor.name,
        company: updated.company,
        detail: { changes: diff },
      });
    }

    return NextResponse.json({ success: true, entry: updated });
  } catch (err) {
    const message = err instanceof Error ? err.message : "Internal error";
    return NextResponse.json({ error: message }, { status: 500 });
  }
}

// ── DELETE — Delete entry ──

export async function DELETE(req: NextRequest) {
  const actor = await authenticateRequest(req);
  if (!actor) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

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

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

  const existing = await loadEntry(id);
  if (!existing) {
    return NextResponse.json({ error: "Not found" }, { status: 404 });
  }

  // Only allow deleting the LAST entry in the sequence (prevent gaps)
  const parsed = parseRegistryNumber(existing.number);
  if (parsed) {
    const companyPrefix =
      parsed.format === "current"
        ? parsed.company
        : REGISTRY_COMPANY_PREFIX[existing.company as CompanyId] ?? parsed.company;
    const yr = parsed.year;
    const oldPrefix = OLD_COMPANY_PREFIX[companyPrefix] ?? "";

    // Find the actual max sequence for this company+year
    // NOTE: JSONB value::text serializes with space after colons
    const newLike = `%"number": "${companyPrefix}-${yr}-%"%`;
    const newRegex = `${companyPrefix}-${yr}-([0-9]{5})`;
    const newMaxRows = await prisma.$queryRaw<Array<{ maxSeq: number | null }>>`
      SELECT MAX(
        CAST(SUBSTRING(value::text FROM ${newRegex}) AS INTEGER)
      ) AS "maxSeq"
      FROM "KeyValueStore"
      WHERE namespace = 'registratura'
        AND key LIKE 'entry:%'
        AND value::text LIKE ${newLike}
    `;
    let maxSeq = newMaxRows[0]?.maxSeq ?? 0;

    // Also check old format
    if (oldPrefix) {
      const oldLike = `%"number": "${oldPrefix}-${yr}-%"%`;
      const oldRegex = `${oldPrefix}-${yr}-(?:IN|OUT|INT)-([0-9]{5})`;
      const oldMaxRows = await prisma.$queryRaw<Array<{ maxSeq: number | null }>>`
        SELECT MAX(
          CAST(SUBSTRING(value::text FROM ${oldRegex}) AS INTEGER)
        ) AS "maxSeq"
        FROM "KeyValueStore"
        WHERE namespace = 'registratura'
          AND key LIKE 'entry:%'
          AND value::text LIKE ${oldLike}
      `;
      maxSeq = Math.max(maxSeq, oldMaxRows[0]?.maxSeq ?? 0);
    }

    if (parsed.sequence < maxSeq) {
      return NextResponse.json(
        {
          error: `Nu poți șterge ${existing.number} — există înregistrări cu numere mai mari (max: ${maxSeq}). Doar ultimul număr din secvență poate fi șters.`,
        },
        { status: 409 },
      );
    }
  }

  await deleteEntryFromDB(id);

  // Recalculate counter so next allocation reads the correct max
  await recalculateSequence(
    existing.company as CompanyId,
    existing.direction as RegistryDirection,
  );

  await logAuditEvent({
    entryId: id,
    entryNumber: existing.number,
    action: "deleted",
    actor: actor.id,
    actorName: actor.name,
    company: existing.company,
    detail: { subject: existing.subject },
  });

  return NextResponse.json({ success: true });
}
