import logging
import re
from enum import Enum
from typing import Any, Dict, List, TypeAlias


log = logging.getLogger(__name__)


KEY_CONFIG = "_meta.name"
KEY_DATE_PATTERN = "date"
KEY_PATTERN_ADJACENT = "adjacent"

KEY_SELECT = "select"
KEY_EXTRACT = "extract"
KEY_REFINE = "refine"
ACTIONS = [KEY_SELECT, KEY_EXTRACT, KEY_REFINE]

# regex constants
WORD_BOUNDARY = r"\b"
RE_MMDDYYYY = r"\d{1,2}/\d{1,2}/\d{2,4}"
RE_BBDDYYYY = (
    r"(jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|"
    r"apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|"
    r"sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|"
    r"dec(?:ember)?)\s+(\d{1,2},\s+)?\d{4}"
)

CONTEXT_BUFFER = 150
COMPARE_BUFFER = 50


class ContextManager:
    """manages text context extraction based on configurable patterns"""

    def __init__(self) -> None:
        self._action_exprs: Dict[str, Any] = {}
        self._setup_default_expressions()

    def _setup_default_expressions(self) -> None:
        """initialize default date patterns"""
        patterns = {"mdY": RE_MMDDYYYY, "MdY": RE_BBDDYYYY}
        exprs = {
            name: re.compile(pattern, re.IGNORECASE)
            for name, pattern in patterns.items()
        }
        self._action_exprs["default"] = {KEY_DATE_PATTERN: exprs}

    def extract_content(self, text: str, config: Dict[str, Any]) -> Dict[str, Any]:
        """extract context matches based on configuration"""

        if not text or not config:
            return {}

        actions = self._get_actions(config)
        if not actions:
            return {}

        if not self._is_selected(text, actions):
            return {}

        extract_patterns = actions.get(KEY_EXTRACT, {})
        if not extract_patterns:
            # TODO: manage
            return {}

        refine_patterns = actions.get(KEY_REFINE, {})

        return self._find_matches(text, extract_patterns, refine_patterns)

    def _build_actions(self, config: Dict[str, Any]) -> Dict[str, Any]:
        """build action patterns from config"""
        actions = {}

        for action in ACTIONS:
            if action not in config:
                continue

            actions[action] = {}
            for pattern_name, pattern in config[action].items():
                compiled_pattern = self._compile_pattern(
                    action, pattern_name, pattern, config
                )
                actions[action][pattern_name] = compiled_pattern

        return actions

    def _build_word(self, pattern: str) -> str:
        return f"{WORD_BOUNDARY}{pattern}{WORD_BOUNDARY}"

    def _compile_pattern(
        self, action: str, pattern_name: str, pattern: str, config: Dict[str, Any]
    ) -> re.Pattern:
        """process individual pattern based on action type"""
        if action == KEY_EXTRACT:
            pattern = self._build_word(pattern)

        if action == KEY_REFINE:
            if pattern_name == KEY_DATE_PATTERN and not pattern:
                return self._get_default_date_patterns()
            if pattern_name == KEY_PATTERN_ADJACENT:
                return pattern

        return re.compile(pattern, re.IGNORECASE)

    def _extract_config_key(self, config: Dict[str, Any]) -> str:
        try:
            keys = KEY_CONFIG.split(".")
            return config[keys[0]][keys[1]]
        except Exception as e:
            log.warning(f"malformed input configuration: {e}")
            return ""

    def _extract_context_window(
        self, text: str, start_pos: int, match_len: int, buffer_size: int
    ) -> str:
        """extract content window around match"""
        remaining_buffer = buffer_size - match_len
        context_start = max(0, start_pos - remaining_buffer // 2)
        return text[context_start : context_start + buffer_size]

    def _find_matches(
        self,
        text: str,
        extract_patterns: Dict[str, re.Pattern],
        refine_patterns: Dict[str, Any],
    ) -> List[Dict]:
        """find and filter pattern matches"""
        matches = []

        for search_name, search_expr in extract_patterns.items():
            for match in search_expr.finditer(text):
                start, group = match.start(), match.group()
                compare = self._extract_context_window(
                    text, start, len(group), COMPARE_BUFFER
                )

                if not self._should_refine(group, compare, refine_patterns):
                    context = self._extract_context_window(
                        text, start, len(group), CONTEXT_BUFFER
                    )
                    if self._is_unique(group, context, compare, matches):
                        matches.append(
                            {
                                "loc": start,
                                "match": group,
                                "context": context,  # , 'pattern': extract_name, 'compare': compare
                            }
                        )

        return matches

    def _get_actions(self, config: Dict[str, Any]) -> Dict[str, Any]:
        config_key = self._extract_config_key(config)
        if not config_key:
            return {}

        if config_key in self._action_exprs:
            return self._action_exprs[config_key]

        actions = self._build_actions(config)
        if actions:
            self._action_exprs[config_key] = actions

        return actions

    def _get_default_date_patterns(self) -> Dict[str, re.Pattern]:
        """get default date patterns"""
        default_actions = self._action_exprs.get("default", {})
        return default_actions.get(KEY_DATE_PATTERN, {})

    def _is_selected(self, text: str, actions: Dict[str, Any]) -> bool:
        """find and filter pattern matches"""
        select_exprs = actions.get(KEY_SELECT, {})
        return any(expr.search(text) for expr in select_exprs.values())

    def _is_unique(
        self, group: str, context: str, compare: str, matches: List[Dict[str, Any]]
    ) -> bool:
        group_len = len(group)
        downsize = (CONTEXT_BUFFER - COMPARE_BUFFER) // 2

        for i in range(len(matches) - 1, -1, -1):
            match = matches[i]
            match_len = len(match.get("match", ""))
            match_context = match.get("context", "")

            if group_len > match_len:
                if compare in match_context:
                    matches.pop(i)
            else:
                match_compare = match_context[downsize : downsize + COMPARE_BUFFER]
                if match_compare in context:
                    return False

        return True

    def _matches_refine_pattern(
        self, match: str, context: str, pattern_name: str, pattern: Any
    ) -> bool:
        """check individual remove pattern"""
        if pattern_name == KEY_PATTERN_ADJACENT and isinstance(pattern, str):
            word = self._build_word(match)
            return re.search(
                pattern + r"\s" + word, context, re.IGNORECASE
            ) or re.search(word + r"\s" + pattern, context, re.IGNORECASE)

        if isinstance(pattern, dict):
            return any(expr.search(context) for expr in pattern.values())

        if hasattr(pattern, "search"):
            return pattern.search(context) is not None

        return False

    def _should_refine(
        self, match: str, context: str, remove_patterns: Dict[str, Any]
    ) -> bool:
        """check if match should be removed based on context"""
        for pattern_name, pattern in remove_patterns.items():
            if self._matches_refine_pattern(match, context, pattern_name, pattern):
                return True
        return False
