from __future__ import annotations

import re
import secrets
from datetime import datetime
from typing import Callable, Literal

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.models.financial_record import ExpenseClaim

DocumentNumberKind = Literal["application", "reimbursement", "audit"]

DOCUMENT_NUMBER_PREFIXES: dict[DocumentNumberKind, str] = {
    "application": "A",
    "reimbursement": "R",
    "audit": "D",
}
DOCUMENT_NUMBER_TOKEN_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
DOCUMENT_NUMBER_TOKEN_LENGTH = 8
DOCUMENT_NUMBER_SHORT_BODY = (
    rf"(?:A|R|D)[{DOCUMENT_NUMBER_TOKEN_ALPHABET}]{{{DOCUMENT_NUMBER_TOKEN_LENGTH}}}"
)
DOCUMENT_NUMBER_LEGACY_BODY = (
    rf"(?:AP|RE|AD)-\d{{14}}-[{DOCUMENT_NUMBER_TOKEN_ALPHABET}]{{{DOCUMENT_NUMBER_TOKEN_LENGTH}}}"
)
DOCUMENT_NUMBER_PATTERN = re.compile(
    rf"^(?:{DOCUMENT_NUMBER_SHORT_BODY}|{DOCUMENT_NUMBER_LEGACY_BODY})$",
    flags=re.IGNORECASE,
)
DOCUMENT_NUMBER_EXTRACT_PATTERN = re.compile(
    rf"(?<![A-Z0-9])(?:"
    rf"{DOCUMENT_NUMBER_SHORT_BODY}"
    rf"|{DOCUMENT_NUMBER_LEGACY_BODY}"
    r"|APP-\d{8}-[A-Z0-9]{6}"
    r"|EXP-\d{6}-\d{3}"
    r")(?![A-Z0-9])",
    flags=re.IGNORECASE,
)


def generate_document_token() -> str:
    return "".join(
        secrets.choice(DOCUMENT_NUMBER_TOKEN_ALPHABET)
        for _ in range(DOCUMENT_NUMBER_TOKEN_LENGTH)
    )


def build_document_number(
    kind: DocumentNumberKind,
    *,
    timestamp: datetime | None = None,
    token: str | None = None,
) -> str:
    prefix = DOCUMENT_NUMBER_PREFIXES[kind]
    normalized_token = (token or generate_document_token()).strip().upper()
    if not re.fullmatch(
        rf"[{DOCUMENT_NUMBER_TOKEN_ALPHABET}]{{{DOCUMENT_NUMBER_TOKEN_LENGTH}}}",
        normalized_token,
    ):
        raise ValueError("document number token must be 8 chars from the configured alphabet")
    return f"{prefix}{normalized_token}"


def generate_unique_expense_claim_no(
    db: Session,
    kind: DocumentNumberKind,
    *,
    timestamp: datetime | None = None,
    token_factory: Callable[[], str] = generate_document_token,
    max_attempts: int = 8,
) -> str:
    for _ in range(max_attempts):
        candidate = build_document_number(
            kind,
            timestamp=timestamp,
            token=token_factory(),
        )
        exists = db.scalar(
            select(ExpenseClaim.id)
            .where(ExpenseClaim.claim_no == candidate)
            .limit(1)
        )
        if exists is None:
            return candidate
    raise RuntimeError(f"failed to generate a unique {kind} document number")


def is_application_claim_no(value: object) -> bool:
    normalized = str(value or "").strip().upper()
    return bool(
        re.fullmatch(
            rf"A[{DOCUMENT_NUMBER_TOKEN_ALPHABET}]{{{DOCUMENT_NUMBER_TOKEN_LENGTH}}}",
            normalized,
        )
    ) or normalized.startswith(("AP-", "APP-"))
