"""
Context-Aware Extraction Module

Extracts field values using contextual cues and label detection.
Improves extraction accuracy by understanding the semantic context.
"""

import re
from typing import Optional, NamedTuple
from dataclasses import dataclass

from .text_cleaner import TextCleaner
from .validators import FieldValidators


@dataclass
class ExtractionCandidate:
    """A candidate extracted value with metadata."""
    value: str
    raw_text: str
    context_label: str
    confidence: float
    position: int  # Character position in source text
    extraction_method: str  # 'label', 'pattern', 'proximity'


class ContextExtractor:
    """
    Context-aware field extraction.

    Uses multiple strategies:
    1. Label detection - finds values after field labels
    2. Pattern matching - uses field-specific regex patterns
    3. Proximity analysis - finds values near related terms
    4. Validation filtering - removes invalid candidates
    """

    # =========================================================================
    # Swedish Label Patterns (what appears before the value)
    # =========================================================================

    LABEL_PATTERNS = {
        'InvoiceNumber': [
            # Swedish
            r'(?:faktura|fakt)\.?\s*(?:nr|nummer|#)?[:\s]*',
            r'(?:fakturanummer|fakturanr)[:\s]*',
            r'(?:vår\s+referens)[:\s]*',
            # English
            r'(?:invoice)\s*(?:no|number|#)?[:\s]*',
            r'inv[.:\s]*#?',
        ],
        'Amount': [
            # Swedish
            r'(?:att\s+)?betala[:\s]*',
            r'(?:total|totalt|summa)[:\s]*',
            r'(?:belopp)[:\s]*',
            r'(?:slutsumma)[:\s]*',
            r'(?:att\s+erlägga)[:\s]*',
            # English
            r'(?:total|amount|sum)[:\s]*',
            r'(?:amount\s+due)[:\s]*',
        ],
        'InvoiceDate': [
            # Swedish
            r'(?:faktura)?datum[:\s]*',
            r'(?:fakt\.?\s*datum)[:\s]*',
            # English
            r'(?:invoice\s+)?date[:\s]*',
        ],
        'InvoiceDueDate': [
            # Swedish
            r'(?:förfall(?:o)?datum)[:\s]*',
            r'(?:betalas\s+senast)[:\s]*',
            r'(?:sista\s+betalningsdag)[:\s]*',
            r'(?:förfaller)[:\s]*',
            # English
            r'(?:due\s+date)[:\s]*',
            r'(?:payment\s+due)[:\s]*',
        ],
        'OCR': [
            r'(?:ocr)[:\s]*',
            r'(?:ocr\s*-?\s*nummer)[:\s]*',
            r'(?:referens(?:nummer)?)[:\s]*',
            r'(?:betalningsreferens)[:\s]*',
        ],
        'Bankgiro': [
            r'(?:bankgiro|bg)[:\s]*',
            r'(?:bank\s*giro)[:\s]*',
        ],
        'Plusgiro': [
            r'(?:plusgiro|pg)[:\s]*',
            r'(?:plus\s*giro)[:\s]*',
            r'(?:postgiro)[:\s]*',
        ],
        'supplier_organisation_number': [
            r'(?:org\.?\s*(?:nr|nummer)?)[:\s]*',
            r'(?:organisationsnummer)[:\s]*',
            r'(?:org\.?\s*id)[:\s]*',
            r'(?:vat\s*(?:no|number|nr)?)[:\s]*',
            r'(?:moms(?:reg)?\.?\s*(?:nr|nummer)?)[:\s]*',
            r'(?:se)[:\s]*',  # VAT prefix
        ],
        'customer_number': [
            r'(?:kund(?:nr|nummer)?)[:\s]*',
            r'(?:kundnummer)[:\s]*',
            r'(?:customer\s*(?:no|number|id)?)[:\s]*',
            r'(?:er\s+referens)[:\s]*',
        ],
    }

    # =========================================================================
    # Value Patterns (what the value looks like)
    # =========================================================================

    VALUE_PATTERNS = {
        'InvoiceNumber': [
            r'[A-Z]{0,3}\d{3,15}',  # Alphanumeric: INV12345
            r'\d{3,15}',            # Pure digits
            r'20\d{2}[-/]\d{3,8}',  # Year prefix: 2024-001
        ],
        'Amount': [
            r'\d{1,3}(?:[\s.]\d{3})*[,]\d{2}',  # Swedish: 1 234,56
            r'\d{1,3}(?:[,]\d{3})*[.]\d{2}',    # US: 1,234.56
            r'\d+[,.]\d{2}',                     # Simple: 123,45
            r'\d+',                              # Integer
        ],
        'InvoiceDate': [
            r'\d{4}[-/.]\d{1,2}[-/.]\d{1,2}',  # ISO-like
            r'\d{1,2}[-/.]\d{1,2}[-/.]\d{4}',  # European
            r'\d{8}',                           # Compact YYYYMMDD
        ],
        'InvoiceDueDate': [
            r'\d{4}[-/.]\d{1,2}[-/.]\d{1,2}',
            r'\d{1,2}[-/.]\d{1,2}[-/.]\d{4}',
            r'\d{8}',
        ],
        'OCR': [
            r'\d{10,25}',  # Long digit sequence
        ],
        'Bankgiro': [
            r'\d{3,4}[-\s]?\d{4}',  # XXX-XXXX or XXXX-XXXX
            r'\d{7,8}',             # Without separator
        ],
        'Plusgiro': [
            r'\d{1,7}[-\s]?\d',     # XXXXXXX-X
            r'\d{2,8}',             # Without separator
        ],
        'supplier_organisation_number': [
            r'\d{6}[-\s]?\d{4}',    # NNNNNN-NNNN
            r'\d{10}',              # Without separator
            r'SE\s?\d{10,12}(?:\s?01)?',  # VAT format
        ],
        'customer_number': [
            r'[A-Z]{0,5}\s?[-]?\s?\d{1,10}',  # EMM 256-6
            r'\d{3,15}',                       # Pure digits
        ],
    }

    # =========================================================================
    # Extraction Methods
    # =========================================================================

    @classmethod
    def extract_with_label(
        cls,
        text: str,
        field_name: str,
        validate: bool = True
    ) -> list[ExtractionCandidate]:
        """
        Extract field values by finding labels and taking following values.

        Example: "Fakturanummer: 12345" -> extracts "12345"
        """
        candidates = []
        label_patterns = cls.LABEL_PATTERNS.get(field_name, [])
        value_patterns = cls.VALUE_PATTERNS.get(field_name, [])

        for label_pattern in label_patterns:
            for value_pattern in value_patterns:
                # Combine label + value patterns
                full_pattern = f'({label_pattern})({value_pattern})'
                matches = re.finditer(full_pattern, text, re.IGNORECASE)

                for match in matches:
                    label = match.group(1).strip()
                    value = match.group(2).strip()

                    # Validate if requested
                    if validate and not cls._validate_value(field_name, value):
                        continue

                    # Calculate confidence based on label specificity
                    confidence = cls._calculate_label_confidence(label, field_name)

                    candidates.append(ExtractionCandidate(
                        value=value,
                        raw_text=match.group(0),
                        context_label=label,
                        confidence=confidence,
                        position=match.start(),
                        extraction_method='label'
                    ))

        return candidates

    @classmethod
    def extract_with_pattern(
        cls,
        text: str,
        field_name: str,
        validate: bool = True
    ) -> list[ExtractionCandidate]:
        """
        Extract field values using only value patterns (no label required).

        This is a fallback when no labels are found.
        """
        candidates = []
        value_patterns = cls.VALUE_PATTERNS.get(field_name, [])

        for pattern in value_patterns:
            matches = re.finditer(pattern, text, re.IGNORECASE)

            for match in matches:
                value = match.group(0).strip()

                # Validate if requested
                if validate and not cls._validate_value(field_name, value):
                    continue

                # Lower confidence for pattern-only extraction
                confidence = 0.6

                candidates.append(ExtractionCandidate(
                    value=value,
                    raw_text=value,
                    context_label='',
                    confidence=confidence,
                    position=match.start(),
                    extraction_method='pattern'
                ))

        return candidates

    @classmethod
    def extract_field(
        cls,
        text: str,
        field_name: str,
        validate: bool = True
    ) -> list[ExtractionCandidate]:
        """
        Extract all candidate values for a field using multiple strategies.

        Returns candidates sorted by confidence (highest first).
        """
        candidates = []

        # Strategy 1: Label-based extraction (highest confidence)
        label_candidates = cls.extract_with_label(text, field_name, validate)
        candidates.extend(label_candidates)

        # Strategy 2: Pattern-based extraction (fallback)
        if not label_candidates:
            pattern_candidates = cls.extract_with_pattern(text, field_name, validate)
            candidates.extend(pattern_candidates)

        # Remove duplicates (same value, keep highest confidence)
        seen_values = {}
        for candidate in candidates:
            normalized = TextCleaner.normalize_for_comparison(candidate.value)
            if normalized not in seen_values or candidate.confidence > seen_values[normalized].confidence:
                seen_values[normalized] = candidate

        # Sort by confidence
        result = sorted(seen_values.values(), key=lambda x: x.confidence, reverse=True)
        return result

    @classmethod
    def extract_best(
        cls,
        text: str,
        field_name: str,
        validate: bool = True
    ) -> Optional[ExtractionCandidate]:
        """
        Extract the best (highest confidence) candidate for a field.
        """
        candidates = cls.extract_field(text, field_name, validate)
        return candidates[0] if candidates else None

    @classmethod
    def extract_all_fields(cls, text: str) -> dict[str, list[ExtractionCandidate]]:
        """
        Extract all known fields from text.

        Returns a dictionary mapping field names to their candidates.
        """
        results = {}
        for field_name in cls.LABEL_PATTERNS.keys():
            candidates = cls.extract_field(text, field_name)
            if candidates:
                results[field_name] = candidates
        return results

    # =========================================================================
    # Helper Methods
    # =========================================================================

    @classmethod
    def _validate_value(cls, field_name: str, value: str) -> bool:
        """Validate a value based on field type."""
        field_lower = field_name.lower()

        if 'date' in field_lower:
            return FieldValidators.is_valid_date(value)
        elif 'amount' in field_lower:
            return FieldValidators.is_valid_amount(value)
        elif 'bankgiro' in field_lower:
            # Basic format check, not Luhn
            digits = TextCleaner.extract_digits(value, apply_ocr_correction=False)
            return 7 <= len(digits) <= 8
        elif 'plusgiro' in field_lower:
            digits = TextCleaner.extract_digits(value, apply_ocr_correction=False)
            return 2 <= len(digits) <= 8
        elif 'ocr' in field_lower:
            digits = TextCleaner.extract_digits(value, apply_ocr_correction=False)
            return 10 <= len(digits) <= 25
        elif 'org' in field_lower:
            digits = TextCleaner.extract_digits(value, apply_ocr_correction=False)
            return len(digits) >= 10
        else:
            # For other fields, just check it's not empty
            return bool(value.strip())

    @classmethod
    def _calculate_label_confidence(cls, label: str, field_name: str) -> float:
        """
        Calculate confidence based on how specific the label is.

        More specific labels = higher confidence.
        """
        label_lower = label.lower()

        # Very specific labels
        very_specific = {
            'InvoiceNumber': ['fakturanummer', 'invoice number', 'fakturanr'],
            'Amount': ['att betala', 'slutsumma', 'amount due'],
            'InvoiceDate': ['fakturadatum', 'invoice date'],
            'InvoiceDueDate': ['förfallodatum', 'förfallodag', 'due date'],
            'OCR': ['ocr', 'betalningsreferens'],
            'Bankgiro': ['bankgiro'],
            'Plusgiro': ['plusgiro', 'postgiro'],
            'supplier_organisation_number': ['organisationsnummer', 'org nummer'],
            'customer_number': ['kundnummer', 'customer number'],
        }

        # Check for very specific match
        if field_name in very_specific:
            for specific in very_specific[field_name]:
                if specific in label_lower:
                    return 0.95

        # Moderately specific
        moderate = {
            'InvoiceNumber': ['faktura', 'invoice', 'nr'],
            'Amount': ['total', 'summa', 'belopp'],
            'InvoiceDate': ['datum', 'date'],
            'InvoiceDueDate': ['förfall', 'due'],
        }

        if field_name in moderate:
            for mod in moderate[field_name]:
                if mod in label_lower:
                    return 0.85

        # Generic match
        return 0.75

    @classmethod
    def find_field_context(cls, text: str, position: int, window: int = 50) -> str:
        """
        Get the surrounding context for a position in text.

        Useful for understanding what field a value belongs to.
        """
        start = max(0, position - window)
        end = min(len(text), position + window)
        return text[start:end]

    @classmethod
    def identify_field_type(cls, text: str, value: str) -> Optional[str]:
        """
        Try to identify what field type a value belongs to based on context.

        Looks at text surrounding the value to find labels.
        """
        # Find the value in text
        pos = text.find(value)
        if pos == -1:
            return None

        # Get context before the value
        context_before = text[max(0, pos - 50):pos].lower()

        # Check each field's labels
        for field_name, patterns in cls.LABEL_PATTERNS.items():
            for pattern in patterns:
                if re.search(pattern, context_before, re.IGNORECASE):
                    return field_name

        return None


# =========================================================================
# Convenience functions
# =========================================================================

def extract_field_with_context(text: str, field_name: str) -> Optional[str]:
    """Convenience function to extract a field value."""
    candidate = ContextExtractor.extract_best(text, field_name)
    return candidate.value if candidate else None


def extract_all_with_context(text: str) -> dict[str, str]:
    """Convenience function to extract all fields."""
    all_candidates = ContextExtractor.extract_all_fields(text)
    return {
        field: candidates[0].value
        for field, candidates in all_candidates.items()
        if candidates
    }
