"""
Database-based Auto-labeling Service

Processes documents with field values stored in the database (csv_field_values).
Used by the pre-label API to create annotations from expected values.
"""

from __future__ import annotations

import logging
from pathlib import Path
from typing import Any

from shared.config import DEFAULT_DPI
from shared.fields import CSV_TO_CLASS_MAPPING
from backend.data.admin_models import AdminDocument
from backend.data.repositories import DocumentRepository, AnnotationRepository
from shared.data.db import DocumentDB
from backend.web.services.storage_helpers import get_storage_helper

logger = logging.getLogger(__name__)

# Initialize DocumentDB for saving match reports
_document_db: DocumentDB | None = None


def get_document_db() -> DocumentDB:
    """Get or create DocumentDB instance with connection and tables initialized.

    Follows the same pattern as CLI autolabel (src/cli/autolabel.py lines 370-373).
    """
    global _document_db
    if _document_db is None:
        _document_db = DocumentDB()
        _document_db.connect()
        _document_db.create_tables()  # Ensure tables exist
        logger.info("Connected to PostgreSQL DocumentDB for match reports")
    return _document_db


def convert_csv_field_values_to_row_dict(
    document: AdminDocument,
) -> dict[str, Any]:
    """
    Convert AdminDocument.csv_field_values to row_dict format for autolabel.

    Args:
        document: AdminDocument with csv_field_values

    Returns:
        Dictionary in row_dict format compatible with autolabel_tasks
    """
    csv_values = document.csv_field_values or {}

    # Build row_dict with DocumentId
    row_dict = {
        "DocumentId": str(document.document_id),
    }

    # Map csv_field_values to row_dict format
    # csv_field_values uses keys like: InvoiceNumber, InvoiceDate, Amount, OCR, Bankgiro, etc.
    # row_dict uses same keys
    for key, value in csv_values.items():
        if value is not None and value != "":
            row_dict[key] = str(value)

    return row_dict


def get_pending_autolabel_documents(
    limit: int = 10,
) -> list[AdminDocument]:
    """
    Get documents pending auto-labeling.

    Args:
        limit: Maximum number of documents to return

    Returns:
        List of AdminDocument records with status='auto_labeling' and auto_label_status='pending'
    """
    from sqlmodel import select
    from backend.data.database import get_session_context
    from backend.data.admin_models import AdminDocument

    with get_session_context() as session:
        statement = select(AdminDocument).where(
            AdminDocument.status == "auto_labeling",
            AdminDocument.auto_label_status == "pending",
        ).order_by(AdminDocument.created_at).limit(limit)

        results = session.exec(statement).all()
        for r in results:
            session.expunge(r)
        return list(results)


def process_document_autolabel(
    document: AdminDocument,
    output_dir: Path | None = None,
    dpi: int = DEFAULT_DPI,
    min_confidence: float = 0.5,
    doc_repo: DocumentRepository | None = None,
    ann_repo: AnnotationRepository | None = None,
) -> dict[str, Any]:
    """
    Process a single document for auto-labeling using csv_field_values.

    Args:
        document: AdminDocument with csv_field_values and file_path
        output_dir: Output directory for temp files
        dpi: Rendering DPI
        min_confidence: Minimum match confidence
        doc_repo: Document repository (created if None)
        ann_repo: Annotation repository (created if None)

    Returns:
        Result dictionary with success status and annotations
    """
    from training.processing.autolabel_tasks import process_text_pdf, process_scanned_pdf
    from shared.pdf import PDFDocument

    # Initialize repositories if not provided
    if doc_repo is None:
        doc_repo = DocumentRepository()
    if ann_repo is None:
        ann_repo = AnnotationRepository()

    document_id = str(document.document_id)
    file_path = Path(document.file_path)

    # Get output directory from StorageHelper
    storage = get_storage_helper()
    if output_dir is None:
        output_dir = storage.get_autolabel_output_path()
        if output_dir is None:
            output_dir = Path("data/autolabel_output")
    output_dir.mkdir(parents=True, exist_ok=True)

    # Mark as processing
    doc_repo.update_status(
        document_id=document_id,
        status="auto_labeling",
        auto_label_status="running",
    )

    try:
        # Check if file exists
        if not file_path.exists():
            raise FileNotFoundError(f"File not found: {file_path}")

        # Convert csv_field_values to row_dict
        row_dict = convert_csv_field_values_to_row_dict(document)

        if len(row_dict) <= 1:  # Only has DocumentId
            raise ValueError("No field values to match")

        # Determine PDF type (text or scanned)
        is_scanned = False
        with PDFDocument(file_path) as pdf_doc:
            # Check if first page has extractable text
            tokens = list(pdf_doc.extract_text_tokens(0))
            is_scanned = len(tokens) < 10  # Threshold for "no text"

        # Build task data
        # Use raw_pdfs base path for pdf_path
        # This ensures consistency with CLI autolabel for reprocess_failed.py
        raw_pdfs_dir = storage.get_raw_pdfs_base_path()
        if raw_pdfs_dir is None:
            raise ValueError("Storage not configured for local access")
        pdf_path_for_report = raw_pdfs_dir / f"{document_id}.pdf"

        task_data = {
            "row_dict": row_dict,
            "pdf_path": str(pdf_path_for_report),
            "output_dir": str(output_dir),
            "dpi": dpi,
            "min_confidence": min_confidence,
        }

        # Process based on PDF type
        if is_scanned:
            result = process_scanned_pdf(task_data)
        else:
            result = process_text_pdf(task_data)

        # Save report to DocumentDB (same as CLI autolabel)
        if result.get("report"):
            try:
                doc_db = get_document_db()
                doc_db.save_document(result["report"])
                logger.info(f"Saved match report to DocumentDB for {document_id}")
            except Exception as e:
                logger.warning(f"Failed to save report to DocumentDB: {e}")

        # Save annotations to database
        if result.get("success") and result.get("report"):
            _save_annotations_to_db(
                ann_repo=ann_repo,
                document_id=document_id,
                report=result["report"],
                page_annotations=result.get("pages", []),
                dpi=dpi,
            )

            # Mark as completed
            doc_repo.update_status(
                document_id=document_id,
                status="labeled",
                auto_label_status="completed",
            )
        else:
            # Mark as failed
            errors = result.get("report", {}).get("errors", ["Unknown error"])
            doc_repo.update_status(
                document_id=document_id,
                status="pending",
                auto_label_status="failed",
                auto_label_error="; ".join(errors) if errors else "No annotations generated",
            )

        return result

    except Exception as e:
        logger.error(f"Error processing document {document_id}: {e}", exc_info=True)

        # Mark as failed
        doc_repo.update_status(
            document_id=document_id,
            status="pending",
            auto_label_status="failed",
            auto_label_error=str(e),
        )

        return {
            "doc_id": document_id,
            "success": False,
            "error": str(e),
        }


def _save_annotations_to_db(
    ann_repo: AnnotationRepository,
    document_id: str,
    report: dict[str, Any],
    page_annotations: list[dict[str, Any]],
    dpi: int = 200,
) -> int:
    """
    Save generated annotations to database.

    Args:
        ann_repo: Annotation repository instance
        document_id: Document ID
        report: AutoLabelReport as dict
        page_annotations: List of page annotation data
        dpi: DPI used for rendering images (for coordinate conversion)

    Returns:
        Number of annotations saved
    """
    from shared.fields import FIELD_CLASS_IDS
    from backend.web.services.storage_helpers import get_storage_helper

    # Mapping from CSV field names to internal field names
    CSV_TO_INTERNAL_FIELD: dict[str, str] = {
        "InvoiceNumber": "invoice_number",
        "InvoiceDate": "invoice_date",
        "InvoiceDueDate": "invoice_due_date",
        "OCR": "ocr_number",
        "Bankgiro": "bankgiro",
        "Plusgiro": "plusgiro",
        "Amount": "amount",
        "supplier_organisation_number": "supplier_organisation_number",
        "customer_number": "customer_number",
        "payment_line": "payment_line",
    }

    # Scale factor: PDF points (72 DPI) -> pixels (at configured DPI)
    scale = dpi / 72.0

    # Get storage helper for image dimensions
    storage = get_storage_helper()

    # Cache for image dimensions per page
    image_dimensions: dict[int, tuple[int, int]] = {}

    def get_image_dimensions(page_no: int) -> tuple[int, int] | None:
        """Get image dimensions for a page (1-indexed)."""
        if page_no in image_dimensions:
            return image_dimensions[page_no]

        # Get dimensions from storage helper
        dims = storage.get_admin_image_dimensions(document_id, page_no)
        if dims:
            image_dimensions[page_no] = dims
            return dims

        return None

    annotation_count = 0

    # Get field results from report (list of dicts)
    field_results = report.get("field_results", [])

    for field_info in field_results:
        if not field_info.get("matched"):
            continue

        csv_field_name = field_info.get("field_name", "")

        # Map CSV field name to internal field name
        field_name = CSV_TO_INTERNAL_FIELD.get(csv_field_name, csv_field_name)

        # Get class_id from field name
        class_id = FIELD_CLASS_IDS.get(field_name)
        if class_id is None:
            logger.warning(f"Unknown field name: {csv_field_name} -> {field_name}")
            continue

        # Get bbox info (list: [x, y, x2, y2] in PDF points - 72 DPI)
        bbox = field_info.get("bbox", [])
        if not bbox or len(bbox) < 4:
            continue

        # Convert PDF points (72 DPI) to pixel coordinates (at configured DPI)
        pdf_x1, pdf_y1, pdf_x2, pdf_y2 = bbox[0], bbox[1], bbox[2], bbox[3]
        x1 = pdf_x1 * scale
        y1 = pdf_y1 * scale
        x2 = pdf_x2 * scale
        y2 = pdf_y2 * scale

        bbox_width = x2 - x1
        bbox_height = y2 - y1

        # Get page number (convert to 1-indexed)
        page_no = field_info.get("page_no", 0) + 1

        # Get image dimensions for normalization
        dims = get_image_dimensions(page_no)
        if dims:
            img_width, img_height = dims
            # Calculate normalized coordinates
            x_center = (x1 + x2) / 2 / img_width
            y_center = (y1 + y2) / 2 / img_height
            width = bbox_width / img_width
            height = bbox_height / img_height
        else:
            # Fallback: use pixel coordinates as-is for normalization
            # (will be slightly off but better than /1000)
            logger.warning(f"Could not get image dimensions for page {page_no}, using estimates")
            # Estimate A4 at configured DPI: 595 x 842 points * scale
            estimated_width = 595 * scale
            estimated_height = 842 * scale
            x_center = (x1 + x2) / 2 / estimated_width
            y_center = (y1 + y2) / 2 / estimated_height
            width = bbox_width / estimated_width
            height = bbox_height / estimated_height

        # Create annotation
        try:
            ann_repo.create(
                document_id=document_id,
                page_number=page_no,
                class_id=class_id,
                class_name=field_name,
                x_center=x_center,
                y_center=y_center,
                width=width,
                height=height,
                bbox_x=int(x1),
                bbox_y=int(y1),
                bbox_width=int(bbox_width),
                bbox_height=int(bbox_height),
                text_value=field_info.get("matched_text"),
                confidence=field_info.get("score"),
                source="auto",
            )
            annotation_count += 1
            logger.info(f"Saved annotation for {field_name}: bbox=({int(x1)}, {int(y1)}, {int(bbox_width)}, {int(bbox_height)})")
        except Exception as e:
            logger.warning(f"Failed to save annotation for {field_name}: {e}")

    return annotation_count


def run_pending_autolabel_batch(
    batch_size: int = 10,
    output_dir: Path | None = None,
    doc_repo: DocumentRepository | None = None,
    ann_repo: AnnotationRepository | None = None,
) -> dict[str, Any]:
    """
    Process a batch of pending auto-label documents.

    Args:
        batch_size: Number of documents to process
        output_dir: Output directory for temp files
        doc_repo: Document repository (created if None)
        ann_repo: Annotation repository (created if None)

    Returns:
        Summary of processing results
    """
    if doc_repo is None:
        doc_repo = DocumentRepository()
    if ann_repo is None:
        ann_repo = AnnotationRepository()

    documents = get_pending_autolabel_documents(limit=batch_size)

    results = {
        "total": len(documents),
        "successful": 0,
        "failed": 0,
        "documents": [],
    }

    for doc in documents:
        result = process_document_autolabel(
            document=doc,
            output_dir=output_dir,
            doc_repo=doc_repo,
            ann_repo=ann_repo,
        )

        doc_result = {
            "document_id": str(doc.document_id),
            "success": result.get("success", False),
        }

        if result.get("success"):
            results["successful"] += 1
        else:
            results["failed"] += 1
            doc_result["error"] = result.get("error") or "Unknown error"

        results["documents"].append(doc_result)

    return results


def save_manual_annotations_to_document_db(
    document: AdminDocument,
    annotations: list,
) -> dict[str, Any]:
    """
    Save manual annotations to PostgreSQL documents and field_results tables.

    Called when user marks a document as 'labeled' from the web UI.
    This ensures manually labeled documents are also tracked in the same
    database as auto-labeled documents for consistency.

    Args:
        document: AdminDocument instance
        annotations: List of AdminAnnotation instances

    Returns:
        Dict with success status and details
    """
    from datetime import datetime

    document_id = str(document.document_id)

    # Build pdf_path using raw_pdfs base path (same as auto-label)
    storage = get_storage_helper()
    raw_pdfs_dir = storage.get_raw_pdfs_base_path()
    if raw_pdfs_dir is None:
        return {
            "success": False,
            "document_id": document_id,
            "error": "Storage not configured for local access",
        }
    pdf_path = raw_pdfs_dir / f"{document_id}.pdf"

    # Build report dict compatible with DocumentDB.save_document()
    field_results = []
    fields_total = len(annotations)
    fields_matched = 0

    for ann in annotations:
        # All manual annotations are considered "matched" since user verified them
        field_result = {
            "field_name": ann.class_name,
            "csv_value": ann.text_value or "",  # Manual annotations may not have CSV value
            "matched": True,
            "score": ann.confidence or 1.0,  # Manual = high confidence
            "matched_text": ann.text_value,
            "candidate_used": "manual",
            "bbox": [ann.bbox_x, ann.bbox_y, ann.bbox_x + ann.bbox_width, ann.bbox_y + ann.bbox_height],
            "page_no": ann.page_number - 1,  # Convert to 0-indexed
            "context_keywords": [],
            "error": None,
        }
        field_results.append(field_result)
        fields_matched += 1

    # Determine PDF type
    pdf_type = "unknown"
    if pdf_path.exists():
        try:
            from shared.pdf import PDFDocument
            with PDFDocument(pdf_path) as pdf_doc:
                tokens = list(pdf_doc.extract_text_tokens(0))
                pdf_type = "scanned" if len(tokens) < 10 else "text"
        except Exception as e:
            logger.warning(f"Could not determine PDF type: {e}")

    # Build report
    report = {
        "document_id": document_id,
        "pdf_path": str(pdf_path),
        "pdf_type": pdf_type,
        "success": fields_matched > 0,
        "total_pages": document.page_count,
        "fields_matched": fields_matched,
        "fields_total": fields_total,
        "annotations_generated": fields_matched,
        "processing_time_ms": 0,  # Manual labeling - no processing time
        "timestamp": datetime.utcnow().isoformat(),
        "errors": [],
        "field_results": field_results,
        # Extended fields (from CSV if available)
        "split": None,
        "customer_number": document.csv_field_values.get("customer_number") if document.csv_field_values else None,
        "supplier_name": document.csv_field_values.get("supplier_name") if document.csv_field_values else None,
        "supplier_organisation_number": document.csv_field_values.get("supplier_organisation_number") if document.csv_field_values else None,
        "supplier_accounts": document.csv_field_values.get("supplier_accounts") if document.csv_field_values else None,
    }

    # Save to PostgreSQL DocumentDB
    try:
        doc_db = get_document_db()
        doc_db.save_document(report)
        logger.info(f"Saved manual annotations to DocumentDB for {document_id}: {fields_matched} fields")

        return {
            "success": True,
            "document_id": document_id,
            "fields_saved": fields_matched,
            "message": f"Saved {fields_matched} annotations to DocumentDB",
        }

    except Exception as e:
        logger.error(f"Failed to save manual annotations to DocumentDB: {e}", exc_info=True)
        return {
            "success": False,
            "document_id": document_id,
            "error": str(e),
        }
