import type { CompanyId } from "@/core/auth/types";
import type { RegistryEntry, RegistryAttachment, RegistryDirection } from "../types";
import { REGISTRY_COMPANY_PREFIX, OLD_COMPANY_PREFIX } from "../types";

const STORAGE_PREFIX = "entry:";

export interface RegistryStorage {
  get<T>(key: string): Promise<T | null>;
  set<T>(key: string, value: T): Promise<void>;
  delete(key: string): Promise<void>;
  list(): Promise<string[]>;
  exportAll(options?: {
    lightweight?: boolean;
  }): Promise<Record<string, unknown>>;
}

// ── Blob separation ──
// Base64 attachment data is stored in a SEPARATE namespace ("registratura-blobs")
// so the main entries are always small (~1-2KB each). PostgreSQL never needs to
// decompress multi-MB TOAST chunks for list queries.

/** Shape of the blob record for one entry */
interface EntryBlobs {
  /** attachmentId → base64 data */
  attachments?: Record<string, string>;
  /** closure attachment base64 */
  closureAttachment?: string;
}

/** Strip base64 from attachments, return stripped entry + extracted blobs */
function extractBlobs(entry: RegistryEntry): {
  stripped: RegistryEntry;
  blobs: EntryBlobs | null;
} {
  const blobs: EntryBlobs = {};
  let hasBlobs = false;

  // Strip attachment data (keep metadata: id, name, type, size, addedAt)
  const strippedAttachments: RegistryAttachment[] = (
    entry.attachments ?? []
  ).map((att) => {
    if (att.data && att.data.length > 1024 && att.data !== "__stripped__") {
      if (!blobs.attachments) blobs.attachments = {};
      blobs.attachments[att.id] = att.data;
      hasBlobs = true;
      return { ...att, data: "" };
    }
    return att;
  });

  let stripped: RegistryEntry = { ...entry, attachments: strippedAttachments };

  // Strip closure attachment data
  if (
    entry.closureInfo?.attachment?.data &&
    entry.closureInfo.attachment.data.length > 1024 &&
    entry.closureInfo.attachment.data !== "__stripped__"
  ) {
    blobs.closureAttachment = entry.closureInfo.attachment.data;
    hasBlobs = true;
    stripped = {
      ...stripped,
      closureInfo: {
        ...entry.closureInfo,
        attachment: { ...entry.closureInfo.attachment, data: "" },
      },
    };
  }

  return { stripped, blobs: hasBlobs ? blobs : null };
}

/** Merge blob data back into a stripped entry */
function mergeBlobs(
  entry: RegistryEntry,
  blobs: EntryBlobs | null,
): RegistryEntry {
  if (!blobs) return entry;

  let merged = entry;

  if (blobs.attachments) {
    merged = {
      ...merged,
      attachments: (merged.attachments ?? []).map((att) => {
        const data = blobs.attachments?.[att.id];
        if (
          data &&
          (!att.data || att.data === "" || att.data === "__stripped__")
        ) {
          return { ...att, data };
        }
        return att;
      }),
    };
  }

  if (blobs.closureAttachment && merged.closureInfo?.attachment) {
    merged = {
      ...merged,
      closureInfo: {
        ...merged.closureInfo,
        attachment: {
          ...merged.closureInfo.attachment,
          data: blobs.closureAttachment,
        },
      },
    };
  }

  return merged;
}

/**
 * Load all registry entries in a SINGLE lightweight request.
 * Uses exportAll({ lightweight: true }) to strip any remaining base64
 * data server-side, ensuring the browser receives only small JSON.
 */
export async function getAllEntries(
  storage: RegistryStorage,
): Promise<RegistryEntry[]> {
  const all = await storage.exportAll({ lightweight: true });
  const entries: RegistryEntry[] = [];
  for (const [key, value] of Object.entries(all)) {
    if (key.startsWith(STORAGE_PREFIX) && value) {
      entries.push(value as RegistryEntry);
    }
  }
  entries.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
  return entries;
}

/**
 * Load a single full entry WITH attachment data (for editing).
 * Loads the lightweight entry + its blob record and merges them.
 */
export async function getFullEntry(
  storage: RegistryStorage,
  blobStorage: RegistryStorage,
  id: string,
): Promise<RegistryEntry | null> {
  const entry = await storage.get<RegistryEntry>(`${STORAGE_PREFIX}${id}`);
  if (!entry) return null;
  const blobs = await blobStorage.get<EntryBlobs>(id);
  return mergeBlobs(entry, blobs);
}

/**
 * Save an entry. Strips base64 from the main entry and stores blobs
 * in the separate blob namespace so list queries stay fast.
 */
export async function saveEntry(
  storage: RegistryStorage,
  blobStorage: RegistryStorage,
  entry: RegistryEntry,
): Promise<void> {
  const { stripped, blobs } = extractBlobs(entry);
  await storage.set(`${STORAGE_PREFIX}${entry.id}`, stripped);
  if (blobs) {
    await blobStorage.set(entry.id, blobs);
  }
}

/**
 * Delete an entry and its blob data.
 */
export async function deleteEntry(
  storage: RegistryStorage,
  blobStorage: RegistryStorage,
  id: string,
): Promise<void> {
  await storage.delete(`${STORAGE_PREFIX}${id}`);
  // Clean up blob data (ignore errors if no blobs exist)
  await blobStorage.delete(id).catch(() => {});
}

// ── Old-format numbering (deprecated — kept for backward compatibility) ──

const COMPANY_PREFIXES: Record<CompanyId, string> = {
  beletage: "B",
  "urban-switch": "US",
  "studii-de-teren": "SDT",
  group: "G",
};

/**
 * @deprecated Use allocateSequenceNumber() via the API instead.
 * Client-side numbering — race-condition prone, kept only for fallback.
 */
export function generateRegistryNumber(
  company: CompanyId,
  _date: string,
  existingEntries: RegistryEntry[],
): string {
  const now = new Date();
  const year = now.getFullYear();
  const prefix = COMPANY_PREFIXES[company];

  const regex = new RegExp(`^${prefix}-(\\d+)/${year}$`);
  let maxNum = 0;

  for (const e of existingEntries) {
    const match = e.number.match(regex);
    if (match?.[1]) {
      const num = parseInt(match[1], 10);
      if (num > maxNum) maxNum = num;
    }
  }

  const nextIndex = maxNum + 1;
  const padded = String(nextIndex).padStart(4, "0");
  return `${prefix}-${padded}/${year}`;
}

// ── Registry numbering (server-side, atomic) ──
//
// Format: B-2026-00001  (single-letter prefix, year, 5-digit sequence)
// Direction (intrat/iesit) is NOT part of the number — shown via icon.
// Sequence is shared across directions within the same company+year.

/**
 * Allocate the next sequence number atomically via PostgreSQL.
 * Format: B-2026-00001
 *
 * Uses an interactive Prisma transaction with pg_advisory_xact_lock to
 * serialize concurrent allocations.  Always reads the **actual max
 * sequence from existing entries** (the source of truth) so the counter
 * never drifts — even after entry deletions, company reassignments, or
 * any other mutation that could leave the RegistrySequence counter stale.
 *
 * The `direction` parameter is kept in the signature for API compat but
 * is NOT used in the number format — all directions share one sequence.
 *
 * Must be called from server-side only (API routes).
 */
export async function allocateSequenceNumber(
  company: CompanyId,
  _direction: RegistryDirection,
  year?: number,
): Promise<{ number: string; sequence: number }> {
  // Dynamic import — prisma is only available server-side
  const { prisma } = await import("@/core/storage/prisma");

  const companyPrefix = REGISTRY_COMPANY_PREFIX[company];
  const yr = year ?? new Date().getFullYear();

  const seq = await prisma.$transaction(async (tx) => {
    // Advisory lock scoped to this transaction — serializes concurrent
    // allocations for the same company/year combination.
    const lockKey = `registratura:${companyPrefix}-${yr}`;
    await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtext(${lockKey}))`;

    // 1. Find actual max sequence from entries stored in KeyValueStore.
    //    This is the SOURCE OF TRUTH.
    //    Search BOTH new format (B-2026-00001) and old format (BTG-2026-OUT-00001)
    //    for backward compat with entries created before the format change.
    const oldPrefix = OLD_COMPANY_PREFIX[companyPrefix] ?? "";

    // New format: B-2026-00001
    // NOTE: Use [0-9] instead of \d — PostgreSQL POSIX regex may not support \d
    // NOTE: JSONB value::text serializes with a space after colons ("number": "...")
    const newLike = `%"number": "${companyPrefix}-${yr}-%"%`;
    const newRegex = `${companyPrefix}-${yr}-([0-9]{5})`;
    const newMaxRows = await tx.$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}
    `;
    const newMax = newMaxRows[0]?.maxSeq ?? 0;

    // Old format: BTG-2026-IN-00001 / BTG-2026-OUT-00001
    let oldMax = 0;
    if (oldPrefix) {
      const oldLike = `%"number": "${oldPrefix}-${yr}-%"%`;
      const oldRegex = `${oldPrefix}-${yr}-(?:IN|OUT|INT)-([0-9]{5})`;
      const oldMaxRows = await tx.$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}
      `;
      oldMax = oldMaxRows[0]?.maxSeq ?? 0;
    }

    const actualMax = Math.max(newMax, oldMax);

    // 2. Next sequence = actual entries max + 1.
    const nextSeq = actualMax + 1;

    // 3. Upsert the counter (type = "SEQ" — shared across directions)
    const seqType = "SEQ";
    await tx.$executeRaw`
      INSERT INTO "RegistrySequence" (id, company, year, type, "lastSeq", "createdAt", "updatedAt")
      VALUES (gen_random_uuid()::text, ${companyPrefix}, ${yr}, ${seqType}, ${nextSeq}, NOW(), NOW())
      ON CONFLICT (company, year, type)
      DO UPDATE SET "lastSeq" = ${nextSeq}, "updatedAt" = NOW()
    `;

    return nextSeq;
  });

  const padded = String(seq).padStart(5, "0");

  return {
    number: `${companyPrefix}-${yr}-${padded}`,
    sequence: seq,
  };
}

/**
 * Recalculate a company's sequence counter to match actual entries in the DB.
 * Called when an entry is reassigned away from a company, so the counter
 * reflects the real max sequence instead of staying artificially high.
 *
 * The `direction` parameter is kept for API compat but ignored — sequence
 * is shared across directions.
 */
export async function recalculateSequence(
  company: CompanyId,
  _direction: RegistryDirection,
  year?: number,
): Promise<void> {
  const { prisma } = await import("@/core/storage/prisma");

  const companyPrefix = REGISTRY_COMPANY_PREFIX[company];
  const yr = year ?? new Date().getFullYear();
  const oldPrefix = OLD_COMPANY_PREFIX[companyPrefix] ?? "";
  const seqType = "SEQ";

  // Find max from new format (B-2026-00001)
  // NOTE: Use [0-9] instead of \d — PostgreSQL POSIX regex may not support \d
  // NOTE: JSONB value::text serializes with a space after colons ("number": "...")
  const newLike = `%"number": "${companyPrefix}-${yr}-%"%`;
  const newRegex = `${companyPrefix}-${yr}-([0-9]{5})`;
  const newRows = 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 actualMax = newRows[0]?.maxSeq ?? 0;

  // Also check old format (BTG-2026-OUT-00001)
  if (oldPrefix) {
    const oldLike = `%"number": "${oldPrefix}-${yr}-%"%`;
    const oldRegex = `${oldPrefix}-${yr}-(?:IN|OUT|INT)-([0-9]{5})`;
    const oldRows = 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}
    `;
    actualMax = Math.max(actualMax, oldRows[0]?.maxSeq ?? 0);
  }

  // Reset the counter to the actual max (or delete if 0)
  if (actualMax === 0) {
    await prisma.$executeRaw`
      DELETE FROM "RegistrySequence"
      WHERE company = ${companyPrefix} AND year = ${yr} AND type = ${seqType}
    `;
    // Also clean up old-format counters
    if (oldPrefix) {
      await prisma.$executeRaw`
        DELETE FROM "RegistrySequence"
        WHERE company = ${oldPrefix} AND year = ${yr}
      `;
    }
  } else {
    await prisma.$executeRaw`
      INSERT INTO "RegistrySequence" (id, company, year, type, "lastSeq", "createdAt", "updatedAt")
      VALUES (gen_random_uuid()::text, ${companyPrefix}, ${yr}, ${seqType}, ${actualMax}, NOW(), NOW())
      ON CONFLICT (company, year, type)
      DO UPDATE SET "lastSeq" = ${actualMax}, "updatedAt" = NOW()
    `;
  }
}

// ── Number format detection + parsing ──

export interface ParsedRegistryNumber {
  company: string;
  year: number;
  type?: string;
  sequence: number;
  format: "current" | "v1" | "legacy";
}

/** Detect whether a number uses the current format (B-2026-00001) */
export function isNewFormat(num: string): boolean {
  return /^[A-Z]-\d{4}-\d{5}$/.test(num);
}

/** Parse a registry number in any supported format */
export function parseRegistryNumber(num: string): ParsedRegistryNumber | null {
  // Current format: B-2026-00001
  const currentMatch = num.match(/^([A-Z])-(\d{4})-(\d{5})$/);
  if (currentMatch) {
    return {
      company: currentMatch[1]!,
      year: parseInt(currentMatch[2]!, 10),
      sequence: parseInt(currentMatch[3]!, 10),
      format: "current",
    };
  }

  // V1 format (3-letter prefix + direction): BTG-2026-IN-00125
  const v1Match = num.match(
    /^(BTG|SDT|USW|GRP)-(\d{4})-(IN|OUT|INT)-(\d{5})$/,
  );
  if (v1Match) {
    return {
      company: v1Match[1]!,
      year: parseInt(v1Match[2]!, 10),
      type: v1Match[3]!,
      sequence: parseInt(v1Match[4]!, 10),
      format: "v1",
    };
  }

  // Legacy format: B-0001/2026
  const legacyMatch = num.match(/^(B|US|SDT|G)-(\d+)\/(\d{4})$/);
  if (legacyMatch) {
    return {
      company: legacyMatch[1]!,
      year: parseInt(legacyMatch[3]!, 10),
      sequence: parseInt(legacyMatch[2]!, 10),
      format: "legacy",
    };
  }

  return null;
}

/** Calculate days overdue (negative = days remaining, positive = overdue) */
export function getOverdueDays(deadline: string | undefined): number | null {
  if (!deadline) return null;
  const now = new Date();
  now.setHours(0, 0, 0, 0);
  const dl = new Date(deadline);
  dl.setHours(0, 0, 0, 0);
  const diff = now.getTime() - dl.getTime();
  return Math.floor(diff / (1000 * 60 * 60 * 24));
}
