"""
Swedish Customer Number Parser

Handles extraction and normalization of Swedish customer numbers.
Uses Strategy Pattern with multiple matching patterns.

Common Swedish customer number formats:
- JTY 576-3
- EMM 256-6
- DWQ 211-X
- FFL 019N
"""

import re
import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional, List

from shared.exceptions import CustomerNumberParseError


@dataclass
class CustomerNumberMatch:
    """Customer number match result."""

    value: str
    """The normalized customer number"""

    pattern_name: str
    """Name of the pattern that matched"""

    confidence: float
    """Confidence score (0.0 to 1.0)"""

    raw_text: str
    """Original text that was matched"""

    position: int = 0
    """Position in text where match was found"""


class CustomerNumberPattern(ABC):
    """Abstract base for customer number patterns."""

    @abstractmethod
    def match(self, text: str) -> Optional[CustomerNumberMatch]:
        """
        Try to match pattern in text.

        Args:
            text: Text to search for customer number

        Returns:
            CustomerNumberMatch if found, None otherwise
        """
        pass

    @abstractmethod
    def format(self, match: re.Match) -> str:
        """
        Format matched groups to standard format.

        Args:
            match: Regex match object

        Returns:
            Formatted customer number string
        """
        pass


class DashFormatPattern(CustomerNumberPattern):
    """
    Pattern: ABC 123-X (with dash)

    Examples: JTY 576-3, EMM 256-6, DWQ 211-X
    """

    PATTERN = re.compile(r'\b([A-Za-z]{2,4})\s+(\d{1,4})-([A-Za-z0-9])\b')

    def match(self, text: str) -> Optional[CustomerNumberMatch]:
        """Match customer number with dash format."""
        match = self.PATTERN.search(text)
        if not match:
            return None

        # Check if it's not a postal code
        full_match = match.group(0)
        if self._is_postal_code(full_match):
            return None

        formatted = self.format(match)
        return CustomerNumberMatch(
            value=formatted,
            pattern_name="DashFormat",
            confidence=0.95,
            raw_text=full_match,
            position=match.start()
        )

    def format(self, match: re.Match) -> str:
        """Format to standard ABC 123-X format."""
        prefix = match.group(1).upper()
        number = match.group(2)
        suffix = match.group(3).upper()
        return f"{prefix} {number}-{suffix}"

    def _is_postal_code(self, text: str) -> bool:
        """Check if text looks like Swedish postal code."""
        # SE 106 43, SE10643, etc.
        return bool(
            text.upper().startswith('SE ') and
            re.match(r'^SE\s+\d{3}\s*\d{2}', text, re.IGNORECASE)
        )


class NoDashFormatPattern(CustomerNumberPattern):
    """
    Pattern: ABC 123X (no dash)

    Examples: Dwq 211X, FFL 019N
    Converts to: DWQ 211-X, FFL 019-N
    """

    PATTERN = re.compile(r'\b([A-Za-z]{2,4})\s+(\d{2,4})([A-Za-z])\b')

    def match(self, text: str) -> Optional[CustomerNumberMatch]:
        """Match customer number without dash."""
        match = self.PATTERN.search(text)
        if not match:
            return None

        # Exclude postal codes
        full_match = match.group(0)
        if self._is_postal_code(full_match):
            return None

        formatted = self.format(match)
        return CustomerNumberMatch(
            value=formatted,
            pattern_name="NoDashFormat",
            confidence=0.90,
            raw_text=full_match,
            position=match.start()
        )

    def format(self, match: re.Match) -> str:
        """Format to standard ABC 123-X format (add dash)."""
        prefix = match.group(1).upper()
        number = match.group(2)
        suffix = match.group(3).upper()
        return f"{prefix} {number}-{suffix}"

    def _is_postal_code(self, text: str) -> bool:
        """Check if text looks like Swedish postal code."""
        return bool(re.match(r'^SE\s*\d{3}\s*\d{2}', text, re.IGNORECASE))


class CompactFormatPattern(CustomerNumberPattern):
    """
    Pattern: ABC123X (compact, no spaces)

    Examples: JTY5763, FFL019N
    """

    PATTERN = re.compile(r'\b([A-Z]{2,4})(\d{3,6})([A-Z]?)\b')

    def match(self, text: str) -> Optional[CustomerNumberMatch]:
        """Match compact customer number format."""
        upper_text = text.upper()
        match = self.PATTERN.search(upper_text)
        if not match:
            return None

        # Filter out SE postal codes
        if match.group(1) == 'SE':
            return None

        formatted = self.format(match)
        return CustomerNumberMatch(
            value=formatted,
            pattern_name="CompactFormat",
            confidence=0.75,
            raw_text=match.group(0),
            position=match.start()
        )

    def format(self, match: re.Match) -> str:
        """Format to ABC123X or ABC123-X format."""
        prefix = match.group(1).upper()
        number = match.group(2)
        suffix = match.group(3).upper()

        if suffix:
            return f"{prefix} {number}-{suffix}"
        else:
            return f"{prefix}{number}"


class GenericAlphanumericPattern(CustomerNumberPattern):
    """
    Generic pattern: Letters + numbers + optional dash/letter

    Examples: EMM 256-6, ABC 123, FFL 019
    """

    PATTERN = re.compile(r'\b([A-Z]{2,4}[\s\-]?\d{1,4}[\s\-]?\d{0,2}[A-Z]?)\b')

    def match(self, text: str) -> Optional[CustomerNumberMatch]:
        """Match generic alphanumeric pattern."""
        upper_text = text.upper()

        all_matches = []
        for match in self.PATTERN.finditer(upper_text):
            matched_text = match.group(1)

            # Filter out pure numbers
            if re.match(r'^\d+$', matched_text):
                continue

            # Filter out Swedish postal codes
            if re.match(r'^SE[\s\-]*\d', matched_text):
                continue

            # Filter out single letter + digit + space + digit (V4 2)
            if re.match(r'^[A-Z]\d\s+\d$', matched_text):
                continue

            # Calculate confidence based on characteristics
            confidence = self._calculate_confidence(matched_text)

            all_matches.append((confidence, matched_text, match.start()))

        if all_matches:
            # Return highest confidence match
            best = max(all_matches, key=lambda x: x[0])
            return CustomerNumberMatch(
                value=best[1].strip(),
                pattern_name="GenericAlphanumeric",
                confidence=best[0],
                raw_text=best[1],
                position=best[2]
            )

        return None

    def format(self, match: re.Match) -> str:
        """Return matched text as-is (already uppercase)."""
        return match.group(1).strip()

    def _calculate_confidence(self, text: str) -> float:
        """Calculate confidence score based on text characteristics."""
        # Require letters AND digits
        has_letters = bool(re.search(r'[A-Z]', text, re.IGNORECASE))
        has_digits = bool(re.search(r'\d', text))

        if not (has_letters and has_digits):
            return 0.0  # Not a valid customer number

        score = 0.5  # Base score

        # Bonus for containing dash
        if '-' in text:
            score += 0.2

        # Bonus for typical format XXX NNN-X
        if re.match(r'^[A-Z]{2,4}\s*\d{1,4}-[A-Z0-9]$', text):
            score += 0.25

        # Bonus for medium length
        if 6 <= len(text) <= 12:
            score += 0.1

        return min(score, 1.0)


class LabeledPattern(CustomerNumberPattern):
    """
    Pattern: Explicit label + customer number

    Examples:
    - "Kundnummer: JTY 576-3"
    - "Customer No: EMM 256-6"
    """

    PATTERN = re.compile(
        r'(?:kund(?:nr|nummer|id)?|ert?\s*(?:kund)?(?:nr|nummer)?|customer\s*(?:no|number|id)?)'
        r'\s*[:\.]?\s*([A-Za-z0-9][\w\s\-]{1,20}?)(?:\s{2,}|\n|$)',
        re.IGNORECASE
    )

    def match(self, text: str) -> Optional[CustomerNumberMatch]:
        """Match customer number with explicit label."""
        match = self.PATTERN.search(text)
        if not match:
            return None

        extracted = match.group(1).strip()
        # Remove trailing punctuation
        extracted = re.sub(r'[\s\.\,\:]+$', '', extracted)

        if extracted and len(extracted) >= 2:
            return CustomerNumberMatch(
                value=extracted.upper(),
                pattern_name="Labeled",
                confidence=0.98,  # Very high confidence when labeled
                raw_text=match.group(0),
                position=match.start()
            )

        return None

    def format(self, match: re.Match) -> str:
        """Return matched customer number."""
        extracted = match.group(1).strip()
        return re.sub(r'[\s\.\,\:]+$', '', extracted).upper()


class CustomerNumberParser:
    """Parser for Swedish customer numbers."""

    def __init__(self):
        """Initialize parser with patterns ordered by specificity."""
        self.patterns: List[CustomerNumberPattern] = [
            LabeledPattern(),        # Highest priority - explicit label
            DashFormatPattern(),     # Standard format with dash
            NoDashFormatPattern(),   # Standard format without dash
            CompactFormatPattern(),  # Compact format
            GenericAlphanumericPattern(),  # Fallback generic pattern
        ]
        self.logger = logging.getLogger(__name__)

    def parse(self, text: str) -> tuple[Optional[str], bool, Optional[str]]:
        """
        Parse customer number from text.

        Args:
            text: Text to search for customer number

        Returns:
            Tuple of (customer_number, is_valid, error_message)
        """
        if not text or not text.strip():
            return None, False, "Empty text"

        text = text.strip()

        # Try each pattern
        all_matches: List[CustomerNumberMatch] = []
        for pattern in self.patterns:
            match = pattern.match(text)
            if match:
                all_matches.append(match)

        # No matches
        if not all_matches:
            return None, False, "No customer number found"

        # Return highest confidence match
        best_match = max(all_matches, key=lambda m: (m.confidence, m.position))
        self.logger.debug(
            f"Customer number matched: {best_match.value} "
            f"(pattern: {best_match.pattern_name}, confidence: {best_match.confidence:.2f})"
        )
        return best_match.value, True, None

    def parse_all(self, text: str) -> List[CustomerNumberMatch]:
        """
        Find all customer numbers in text.

        Useful for cases with multiple potential matches.

        Args:
            text: Text to search

        Returns:
            List of CustomerNumberMatch sorted by confidence (descending)
        """
        if not text or not text.strip():
            return []

        all_matches: List[CustomerNumberMatch] = []
        for pattern in self.patterns:
            match = pattern.match(text)
            if match:
                all_matches.append(match)

        # Sort by confidence (highest first), then by position (later first)
        return sorted(all_matches, key=lambda m: (m.confidence, m.position), reverse=True)
