from __future__ import annotations

from datetime import datetime
import hashlib
import io
from pathlib import Path
import re
import uuid

import fitz
from fastapi import HTTPException, UploadFile, status
from PIL import Image
import pytesseract

from app.core.config import get_settings
from app.services.menu_asset_service import (
    extract_menu_asset_text,
    normalize_asset_name,
    normalize_text_block,
    resolve_asset_kind,
    resolve_asset_mime_type,
    truncate_text,
)
from app.services.tenant_store import FiscalDocumentRecord, SessionIdentity, get_tenant_store


_DATE_PATTERN = re.compile(r"\b(\d{2})[/-](\d{2})[/-](\d{4})\b")
_AMOUNT_PATTERN = re.compile(r"(?<!\d)(\d{1,3}(?:\.\d{3})*,\d{2}|\d+,\d{2})(?!\d)")


def _parse_euro_amount(value: str) -> float | None:
    normalized = value.replace(".", "").replace(",", ".").strip()
    try:
        parsed = float(normalized)
    except ValueError:
        return None
    return parsed if parsed >= 0 else None


def _extract_document_date(text: str, filename: str) -> str | None:
    normalized = normalize_text_block(text)
    for pattern in (
        r"\bdata(?:\s+documento|\s+fattura|\s+ddt|\s+bolla)?\s*[:.]?\s*(\d{2}[/-]\d{2}[/-]\d{4})",
        r"\bdel\s+(\d{2}[/-]\d{2}[/-]\d{4})",
    ):
        match = re.search(pattern, normalized, re.IGNORECASE)
        if match:
            raw_value = match.group(1)
            try:
                parsed = datetime.strptime(raw_value.replace("-", "/"), "%d/%m/%Y").date()
                return parsed.isoformat()
            except ValueError:
                continue

    filename_match = re.search(r"(\d{2})(\d{2})(20\d{2})", Path(filename).stem)
    if filename_match:
        try:
            parsed = datetime.strptime("".join(filename_match.groups()), "%d%m%Y").date()
            return parsed.isoformat()
        except ValueError:
            return None
    return None


def _extract_document_number(text: str, document_type: str, filename: str) -> str | None:
    normalized = normalize_text_block(text)
    patterns = []
    if document_type == "invoice":
        patterns.extend(
            [
                r"\bfattura(?:\s+\w+)?\s*(?:n(?:umero)?\.?|nr\.?)?\s*[:#]?\s*([A-Z0-9][A-Z0-9./-]{3,})",
                r"\bnumero\s+fattura\s*[:#]?\s*([A-Z0-9][A-Z0-9./-]{3,})",
            ]
        )
    elif document_type == "delivery_note":
        patterns.extend(
            [
                r"\b(?:ddt|d\.d\.t\.|bolla|documento di trasporto)\s*(?:n(?:umero)?\.?|nr\.?)?\s*[:#]?\s*([A-Z0-9][A-Z0-9./-]{2,})",
            ]
        )
    patterns.extend(
        [
            r"\bnumero\s+documento\s*[:#]?\s*([A-Z0-9][A-Z0-9./-]{2,})",
            r"\bn(?:umero)?\.?\s*documento\s*[:#]?\s*([A-Z0-9][A-Z0-9./-]{2,})",
        ]
    )

    for pattern in patterns:
        match = re.search(pattern, normalized, re.IGNORECASE)
        if match:
            return match.group(1).strip()

    stem = Path(filename).stem.strip()
    if stem:
        compact_tokens = [token for token in re.split(r"[_\s]+", stem) if token]
        if compact_tokens:
            return compact_tokens[0][:80]
    return None


def _is_company_like_line(line: str) -> bool:
    normalized_line = line.casefold()
    ignored_tokens = {
        "fattura",
        "ddt",
        "bolla",
        "documento di trasporto",
        "cliente",
        "destinatario",
        "destinazione",
        "totale",
        "imponibile",
        "iva",
        "pagamento",
        "codice",
        "descrizione",
        "valuta",
        "rif.",
    }
    if any(token in normalized_line for token in ignored_tokens):
        return False
    word_count = len(re.findall(r"[A-Za-zÀ-ÿ]{2,}", line))
    if word_count < 2:
        return False
    if "%" in line or re.search(r"\b\d{3,}\b", line):
        return False
    digit_ratio = sum(character.isdigit() for character in line) / max(len(line), 1)
    if digit_ratio > 0.2:
        return False
    if len(line) > 80:
        return False
    return True


def _extract_supplier_name(text: str) -> str | None:
    lines = [line.strip() for line in normalize_text_block(text).splitlines() if line.strip()]
    for index, line in enumerate(lines[:30]):
        if "cliente" not in line.casefold():
            continue
        start_index = max(0, index - 10)
        for candidate in lines[start_index:index]:
            if _is_company_like_line(candidate):
                return candidate[:120]

    for line in lines[:18]:
        if _is_company_like_line(line):
            return line[:120]
    return None


def _extract_pdf_header_text(path: Path) -> str:
    try:
        document = fitz.open(str(path))
    except Exception:
        return ""

    try:
        if document.page_count <= 0:
            return ""
        page = document.load_page(0)
        rect = page.rect
        clip = fitz.Rect(
            rect.x0,
            rect.y0,
            rect.x0 + rect.width * 0.45,
            rect.y0 + rect.height * 0.22,
        )
        pixmap = page.get_pixmap(matrix=fitz.Matrix(3, 3), clip=clip, alpha=False)
        with Image.open(io.BytesIO(pixmap.tobytes("png"))) as image:
            extracted = pytesseract.image_to_string(image, lang="ita+eng", config="--psm 6")
        return normalize_text_block(extracted)
    except Exception:
        return ""
    finally:
        document.close()


def _extract_amount_from_line(line: str) -> float | None:
    matches = _AMOUNT_PATTERN.findall(line)
    for match in reversed(matches):
        parsed = _parse_euro_amount(match)
        if parsed is not None:
            return parsed
    return None


def _extract_amount_near_label(lines: list[str], *labels: str) -> float | None:
    compiled_labels = [re.compile(label, re.IGNORECASE) for label in labels]
    for index in range(len(lines) - 1, -1, -1):
        line = lines[index]
        if not any(pattern.search(line) for pattern in compiled_labels):
            continue
        for candidate_index in (index, index - 1, index + 1, index - 2, index + 2):
            if candidate_index < 0 or candidate_index >= len(lines):
                continue
            parsed = _extract_amount_from_line(lines[candidate_index])
            if parsed is not None:
                return parsed
    return None


def _extract_total_amount(text: str) -> float | None:
    normalized = normalize_text_block(text)
    lines = [line.strip() for line in normalized.splitlines() if line.strip()]

    total_due = _extract_amount_near_label(lines, r"\btotale\s+da\s+pagare\b")
    if total_due is not None:
        return total_due

    total_document = _extract_amount_near_label(
        lines,
        r"\btotale\s+documento\b",
        r"\btotale\s+fattura\b",
        r"\bimporto\s+totale\b",
    )
    if total_document is not None:
        return total_document

    patterns = (
        r"\b(?:totale(?:\s+documento|\s+fattura|\s+da\s+pagare)?|importo\s+totale)\s*[:€]?\s*([0-9\.,]+)",
        r"\b(?:totale\s+eur|totale\s+euro)\s*[:€]?\s*([0-9\.,]+)",
    )
    for pattern in patterns:
        match = re.search(pattern, normalized, re.IGNORECASE)
        if not match:
            continue
        parsed = _parse_euro_amount(match.group(1))
        if parsed is not None:
            return parsed
    return None


def _classify_document_type(text: str, filename: str) -> str:
    normalized = f"{normalize_text_block(text)}\n{Path(filename).name}".casefold()
    if any(token in normalized for token in ("documento di trasporto", "d.d.t", "ddt", "bolla")):
        return "delivery_note"
    if any(token in normalized for token in ("fattura", "invoice")):
        return "invoice"
    return "unknown"


def _build_summary(
    *,
    document_type: str,
    document_number: str | None,
    document_date: str | None,
    supplier_name: str | None,
    total_amount: float | None,
    currency: str,
) -> str:
    type_label = {
        "invoice": "Fattura",
        "delivery_note": "Bolla/DDT",
        "unknown": "Documento fiscale da classificare",
    }.get(document_type, "Documento fiscale")
    parts = [type_label]
    if document_number:
        parts.append(f"n. {document_number}")
    if document_date:
        parts.append(f"del {document_date}")
    if supplier_name:
        parts.append(f"emittente {supplier_name}")
    if total_amount is not None:
        rendered = f"{total_amount:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
        parts.append(f"totale {rendered} {currency}")
    return " | ".join(parts)


def find_existing_fiscal_document_by_hash(
    session: SessionIdentity,
    *,
    file_hash: str,
) -> FiscalDocumentRecord | None:
    store = get_tenant_store()
    direct_match = store.get_fiscal_document_by_hash(session.tenant_id, file_hash)
    if direct_match is not None:
        return direct_match

    for candidate in store.list_fiscal_documents(session.tenant_id):
        if candidate.file_hash:
            continue
        storage_path = Path(candidate.storage_path)
        if not storage_path.exists() or not storage_path.is_file():
            continue
        try:
            candidate_hash = hashlib.sha256(storage_path.read_bytes()).hexdigest()
        except OSError:
            continue
        if candidate_hash != file_hash:
            continue
        return store.update_fiscal_document(session.tenant_id, candidate.id, file_hash=file_hash)
    return None


async def ingest_fiscal_document(session: SessionIdentity, upload: UploadFile) -> FiscalDocumentRecord:
    raw_bytes = await upload.read()
    return ingest_fiscal_document_bytes(
        session,
        filename=upload.filename or "documento-fiscale",
        content_type=upload.content_type,
        raw_bytes=raw_bytes,
    )


def ingest_fiscal_document_bytes(
    session: SessionIdentity,
    *,
    filename: str,
    content_type: str | None,
    raw_bytes: bytes,
) -> FiscalDocumentRecord:
    settings = get_settings()
    store = get_tenant_store()
    file_hash = hashlib.sha256(raw_bytes).hexdigest() if raw_bytes else None
    if file_hash:
        existing = find_existing_fiscal_document_by_hash(session, file_hash=file_hash)
        if existing is not None:
            return existing
    display_name = normalize_asset_name(filename, fallback="documento-fiscale")
    mime_type = resolve_asset_mime_type(display_name, content_type)
    kind = resolve_asset_kind(display_name, mime_type)

    if kind == "other":
        raise HTTPException(
            status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
            detail=f"Formato non supportato per {display_name}. Carica PDF, immagini o testo.",
        )

    if not raw_bytes:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"{display_name}: file vuoto.")
    if len(raw_bytes) > settings.menu_asset_max_upload_bytes:
        raise HTTPException(
            status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
            detail=f"{display_name}: supera il limite di {settings.menu_asset_max_upload_bytes // (1024 * 1024)} MB.",
        )

    document_id = f"fdoc_{uuid.uuid4().hex}"
    suffix = Path(display_name).suffix.lower() or (".pdf" if kind == "pdf" else ".txt")
    storage_dir = store.fiscal_documents_directory(session.tenant_id)
    storage_path = storage_dir / f"{document_id}{suffix}"
    storage_path.write_bytes(raw_bytes)

    record = store.create_fiscal_document(
        document_id=document_id,
        tenant_id=session.tenant_id,
        original_name=display_name,
        display_name=display_name,
        mime_type=mime_type,
        kind=kind,
        file_size_bytes=len(raw_bytes),
        file_hash=file_hash,
        storage_path=str(storage_path),
        document_type="unknown",
        status="processing",
        matching_status="not_started",
        review_status="to_review",
    )

    extracted_text = ""
    try:
        extracted_text = extract_menu_asset_text(storage_path, kind)
        cleaned_text = truncate_text(extracted_text, settings.menu_asset_extracted_text_max_chars)
        header_text = _extract_pdf_header_text(storage_path) if kind == "pdf" else ""
        document_type = _classify_document_type(cleaned_text, display_name)
        document_number = _extract_document_number(cleaned_text, document_type, display_name)
        document_date = _extract_document_date(cleaned_text, display_name)
        supplier_name = _extract_supplier_name(header_text) or _extract_supplier_name(cleaned_text)
        total_amount = _extract_total_amount(cleaned_text)
        summary_text = _build_summary(
            document_type=document_type,
            document_number=document_number,
            document_date=document_date,
            supplier_name=supplier_name,
            total_amount=total_amount,
            currency="EUR",
        )
        return store.update_fiscal_document(
            session.tenant_id,
            document_id,
            document_type=document_type,
            document_number=document_number,
            document_date=document_date,
            supplier_name=supplier_name,
            total_amount=total_amount,
            currency="EUR",
            summary_text=summary_text,
            extracted_text=cleaned_text,
            status="ready",
            matching_status="pending",
            review_status="to_review",
        )
    except Exception as exc:
        detail = normalize_text_block(str(exc)) or "Analisi documento fiscale non riuscita."
        return store.update_fiscal_document(
            session.tenant_id,
            document_id,
            extracted_text=truncate_text(extracted_text, settings.menu_asset_extracted_text_max_chars),
            summary_text="Documento fiscale caricato ma non ancora leggibile in modo affidabile.",
            status="error",
            error_detail=truncate_text(detail, 500),
            matching_status="not_started",
            review_status="to_review",
        )
