"""
Line Items Extractor

Extracts structured line items from HTML tables produced by PP-StructureV3.
Handles Swedish invoice formats including reversed tables (header at bottom).
Includes fallback text-based extraction for invoices without detectable table structures.
"""

from pathlib import Path
import re
import logging

logger = logging.getLogger(__name__)

# Import models
from .models import LineItem, LineItemsResult

# Import parsers
from .html_table_parser import (
    HTMLTableParser,
    ColumnMapper,
    COLUMN_MAPPINGS,
    SUMMARY_KEYWORDS,
)

# Import merged cell handler
from .merged_cell_handler import MergedCellHandler

# Re-export for backward compatibility
__all__ = [
    "LineItem",
    "LineItemsResult",
    "LineItemsExtractor",
    "ColumnMapper",
    "HTMLTableParser",
    "COLUMN_MAPPINGS",
    "SUMMARY_KEYWORDS",
]

# Configuration constants
# Minimum keyword matches required to detect a header row
MIN_HEADER_KEYWORD_MATCHES = 2


class LineItemsExtractor:
    """Extract structured line items from HTML tables."""

    def __init__(
        self,
        column_mapper: ColumnMapper | None = None,
        table_detector: "TableDetector | None" = None,
        enable_text_fallback: bool = True,
    ):
        """
        Initialize extractor.

        Args:
            column_mapper: Custom column mapper. Uses default Swedish mappings if None.
            table_detector: Optional shared TableDetector instance (avoids slow re-init).
            enable_text_fallback: Enable text-based extraction as fallback.
        """
        self.parser = HTMLTableParser()
        self.mapper = column_mapper or ColumnMapper()
        self.merged_cell_handler = MergedCellHandler(self.mapper)
        self._table_detector = table_detector
        self.enable_text_fallback = enable_text_fallback

    def extract(self, html: str) -> LineItemsResult:
        """
        Extract line items from HTML table.

        Args:
            html: HTML string containing table.

        Returns:
            LineItemsResult with extracted items.
        """
        header, rows = self.parser.parse(html)

        # Check for merged header (rental invoice pattern)
        if self.merged_cell_handler.has_merged_header(header):
            logger.debug("Detected merged header, using merged cell extraction")
            items = self.merged_cell_handler.extract_from_merged_cells(header, rows)
            return LineItemsResult(
                items=items,
                header_row=header,
                raw_html=html,
                is_reversed=False,
            )

        # Check if merged header in first row (no explicit header)
        if not header and rows and self.merged_cell_handler.has_merged_header(rows[0]):
            logger.debug("Detected merged header in first row")
            items = self.merged_cell_handler.extract_from_merged_cells(rows[0], rows[1:])
            return LineItemsResult(
                items=items,
                header_row=rows[0],
                raw_html=html,
                is_reversed=False,
            )

        # Check for vertically merged cells
        if self.merged_cell_handler.has_vertically_merged_cells(rows):
            logger.debug("Detected vertically merged cells, splitting rows")
            header, rows = self.merged_cell_handler.split_merged_rows(rows)

        # If no explicit header, try to detect it
        is_reversed = False
        if not header and rows:
            header_idx, detected_header, is_at_end = self._detect_header_row(rows)
            if header_idx >= 0:
                header = detected_header
                is_reversed = is_at_end
                if is_at_end:
                    # Reversed table: header at bottom
                    rows = rows[:header_idx]
                else:
                    rows = rows[header_idx + 1:]

        # Map columns
        column_map = self.mapper.map(header)

        if not column_map:
            # Couldn't identify columns
            return LineItemsResult(
                items=[],
                header_row=header,
                raw_html=html,
                is_reversed=is_reversed,
            )

        # Extract items
        items = self._extract_items(rows, column_map)

        return LineItemsResult(
            items=items,
            header_row=header,
            raw_html=html,
            is_reversed=is_reversed,
        )

    def extract_from_pdf(self, pdf_path: str | Path) -> LineItemsResult | None:
        """
        Extract line items from PDF using table detection.

        Args:
            pdf_path: Path to PDF file.

        Returns:
            LineItemsResult if tables found, None otherwise.
        """
        from .structure_detector import TableDetector

        # Use shared detector or create new one
        detector = self._table_detector or TableDetector()

        # Detect tables in PDF
        tables, parsing_res_list = self._detect_tables_with_parsing(detector, str(pdf_path))

        # Try structured table extraction first
        for table_result in tables:
            if not table_result.html:
                continue

            # Check if this looks like a line items table
            header, _ = self.parser.parse(table_result.html)
            if self.is_line_items_table(header):
                result = self.extract(table_result.html)
                if result.items:
                    return result

        # Fallback to text-based extraction if enabled
        if self.enable_text_fallback and parsing_res_list:
            return self._try_text_fallback(parsing_res_list)

        return None

    def _detect_tables_with_parsing(
        self, detector: "TableDetector", pdf_path: str
    ) -> tuple[list, list]:
        """
        Detect tables in PDF and return both table results and parsing_res.

        Returns:
            Tuple of (table_results, parsing_res_list)
        """
        from shared.pdf.renderer import render_pdf_to_images
        from PIL import Image
        import io
        import numpy as np

        pdf_path = Path(pdf_path)
        if not pdf_path.exists():
            logger.warning(f"PDF not found: {pdf_path}")
            return [], []
        if not pdf_path.is_file():
            logger.warning(f"Path is not a file: {pdf_path}")
            return [], []

        # Ensure detector is initialized
        detector._ensure_initialized()

        # Render first page
        parsing_res_list = []
        for page_no, image_bytes in render_pdf_to_images(str(pdf_path), dpi=300):
            if page_no == 0:
                image = Image.open(io.BytesIO(image_bytes))
                image_array = np.array(image)

                # Detect tables using shared detector
                tables = detector.detect(image_array)

                # Also get parsing results for text fallback
                if detector._pipeline is not None:
                    try:
                        result = detector._pipeline.predict(image_array)
                        # Extract parsing_res from result (API varies by version)
                        if isinstance(result, dict) and "parsing_res_list" in result:
                            parsing_res_list = result.get("parsing_res_list", [])
                        elif hasattr(result, "parsing_res_list"):
                            parsing_res_list = result.parsing_res_list or []
                    except Exception as e:
                        logger.debug(f"Could not get parsing_res: {e}")

                return tables, parsing_res_list

        return [], []

    def _try_text_fallback(self, parsing_res_list: list) -> LineItemsResult | None:
        """
        Try text-based extraction from parsing results.

        Args:
            parsing_res_list: Parsing results from PP-StructureV3.

        Returns:
            LineItemsResult if extraction successful, None otherwise.
        """
        from .text_line_items_extractor import TextLineItemsExtractor, convert_text_line_item

        text_extractor = TextLineItemsExtractor()
        text_result = text_extractor.extract_from_parsing_res(parsing_res_list)

        if text_result and text_result.items:
            # Convert TextLineItem to LineItem
            items = [convert_text_line_item(item) for item in text_result.items]
            return LineItemsResult(
                items=items,
                header_row=text_result.header_row,
                raw_html="",  # No HTML for text-based extraction
                is_reversed=False,
            )

        return None

    def is_line_items_table(self, header: list[str]) -> bool:
        """
        Check if header indicates a line items table (vs summary/payment table).

        Args:
            header: List of column header strings.

        Returns:
            True if this appears to be a line items table.
        """
        if not header:
            return False

        header_text = " ".join(h.lower() for h in header)

        # Check for summary table keywords (NOT a line items table)
        for keyword in SUMMARY_KEYWORDS:
            if keyword in header_text:
                return False

        # Check for line items keywords
        column_map = self.mapper.map(header)
        has_description = "description" in column_map.values()
        has_amount = "amount" in column_map.values()

        return has_description or has_amount

    def _detect_header_row(
        self, rows: list[list[str]]
    ) -> tuple[int, list[str], bool]:
        """
        Detect which row is the header row.

        PP-StructureV3 sometimes places headers at the bottom (reversed tables).

        Returns:
            (header_index, header_row, is_at_end)
        """
        header_keywords = set()
        for patterns in COLUMN_MAPPINGS.values():
            header_keywords.update(patterns)

        best_match = (-1, [], 0)  # (index, row, match_count)

        for i, row in enumerate(rows):
            # Skip empty rows
            if not any(cell.strip() for cell in row):
                continue

            row_text = " ".join(cell.lower() for cell in row)
            matches = sum(1 for kw in header_keywords if kw in row_text)

            if matches > best_match[2]:
                best_match = (i, row, matches)

        if best_match[2] >= MIN_HEADER_KEYWORD_MATCHES:
            header_idx = best_match[0]
            is_at_end = header_idx == len(rows) - 1 or header_idx > len(rows) // 2
            return header_idx, best_match[1], is_at_end

        return -1, [], False

    def _extract_items(
        self, rows: list[list[str]], column_map: dict[int, str]
    ) -> list[LineItem]:
        """
        Extract line items from rows using column mapping.

        Args:
            rows: Data rows (excluding header).
            column_map: Mapping of column index to field name.

        Returns:
            List of LineItem objects.
        """
        items = []

        for row_idx, row in enumerate(rows):
            # Skip empty rows
            if not any(cell.strip() for cell in row):
                continue

            item_data = {"row_index": row_idx}

            for col_idx, field_name in column_map.items():
                if col_idx < len(row):
                    value = row[col_idx].strip()
                    if value:
                        item_data[field_name] = value

            # Check for deduction
            is_deduction = False
            description = item_data.get("description", "")
            amount = item_data.get("amount", "")

            if description:
                desc_lower = description.lower()
                if any(kw in desc_lower for kw in ["avdrag", "rabatt", "kredit"]):
                    is_deduction = True

            if amount and amount.startswith("-"):
                is_deduction = True

            # Create line item if we have at least description or amount
            if item_data.get("description") or item_data.get("amount"):
                item = LineItem(
                    row_index=row_idx,
                    description=item_data.get("description"),
                    quantity=item_data.get("quantity"),
                    unit=item_data.get("unit"),
                    unit_price=item_data.get("unit_price"),
                    amount=item_data.get("amount"),
                    article_number=item_data.get("article_number"),
                    vat_rate=item_data.get("vat_rate"),
                    is_deduction=is_deduction,
                )
                items.append(item)

        return items

    # Backward compatibility: expose merged cell handler methods
    def _has_merged_header(self, header: list[str] | None) -> bool:
        """Check if header appears to be merged. Delegates to MergedCellHandler."""
        return self.merged_cell_handler.has_merged_header(header)

    def _has_vertically_merged_cells(self, rows: list[list[str]]) -> bool:
        """Check for vertically merged cells. Delegates to MergedCellHandler."""
        return self.merged_cell_handler.has_vertically_merged_cells(rows)

    def _split_merged_rows(
        self, rows: list[list[str]]
    ) -> tuple[list[str], list[list[str]]]:
        """Split merged rows. Delegates to MergedCellHandler."""
        return self.merged_cell_handler.split_merged_rows(rows)

    def _extract_from_merged_cells(
        self, header: list[str], rows: list[list[str]]
    ) -> list[LineItem]:
        """Extract from merged cells. Delegates to MergedCellHandler."""
        return self.merged_cell_handler.extract_from_merged_cells(header, rows)
