#!/usr/bin/env python3
"""
FUZZY Executor
Company: eXonware.com
Author: eXonware Backend Team
Email: connect@exonware.com
Version: 0.9.0.6
"""

import re
from typing import Any, Optional
from ..base import AUniversalOperationExecutor
from ....contracts import QueryAction, ExecutionContext, ExecutionResult
from ....defs import OperationType
from ..utils import extract_items, extract_field_value


class FuzzyExecutor(AUniversalOperationExecutor):
    """
    Filter items by Levenshtein distance between field value and target value.
    Time complexity: O(n * m * k), where n=items, m/k=string lengths.
    """

    OPERATION_NAME = "FUZZY"
    OPERATION_TYPE = OperationType.FILTERING
    SUPPORTED_NODE_TYPES = []

    def _do_execute(self, action: QueryAction, context: ExecutionContext) -> ExecutionResult:
        params = action.params
        field = params.get("field")
        value = params.get("value")
        max_distance = params.get("max_distance", 2)
        as_field = params.get("as_field", None)
        path = params.get("path", None)

        if not field:
            return ExecutionResult(
                success=False,
                data=None,
                error=f"FUZZY requires 'field'. Got: {action.params}",
                action_type=self.OPERATION_NAME,
            )

        if value is None:
            return ExecutionResult(
                success=False,
                data=None,
                error=f"FUZZY requires 'value'. Got: {action.params}",
                action_type=self.OPERATION_NAME,
            )

        try:
            max_distance_int = int(max_distance)
        except (TypeError, ValueError):
            return ExecutionResult(
                success=False,
                data=None,
                error=f"FUZZY 'max_distance' must be int. Got: {max_distance}",
                action_type=self.OPERATION_NAME,
            )

        result_data = self._execute_fuzzy(
            context.node,
            field,
            value,
            max_distance_int,
            as_field,
            path,
        )
        return ExecutionResult(
            success=True,
            data=result_data,
            action_type=self.OPERATION_NAME,
            metadata={"matched_count": len(result_data.get("items", []))},
        )

    @staticmethod
    def _normalize_text(value: Any) -> str:
        return re.sub(r"\s+", " ", str(value).strip())

    @staticmethod
    def _levenshtein_distance(left: str, right: str) -> int:
        if left == right:
            return 0
        if not left:
            return len(right)
        if not right:
            return len(left)

        if len(left) > len(right):
            left, right = right, left

        previous_row = list(range(len(left) + 1))
        for i, right_char in enumerate(right, start=1):
            current_row = [i]
            for j, left_char in enumerate(left, start=1):
                insert_cost = current_row[j - 1] + 1
                delete_cost = previous_row[j] + 1
                replace_cost = previous_row[j - 1] + (left_char != right_char)
                current_row.append(min(insert_cost, delete_cost, replace_cost))
            previous_row = current_row
        return previous_row[-1]

    def _execute_fuzzy(
        self,
        node: Any,
        field: str,
        value: Any,
        max_distance: int,
        as_field: Optional[str],
        path: Optional[str],
    ) -> dict:
        if path:
            try:
                data = node.get(path, default=None)
            except Exception:
                data = None
        else:
            data = node

        items = extract_items(data)
        target_text = self._normalize_text(value)
        matched_items = []

        for item in items:
            field_value = extract_field_value(item, field)
            if field_value is None:
                continue

            candidate_text = self._normalize_text(field_value)
            distance = self._levenshtein_distance(candidate_text, target_text)
            if distance <= max_distance:
                if as_field:
                    if isinstance(item, dict):
                        out_item = dict(item)
                        out_item[as_field] = distance
                        matched_items.append(out_item)
                    else:
                        try:
                            setattr(item, as_field, distance)
                        except Exception:
                            pass
                        matched_items.append(item)
                else:
                    matched_items.append(item)

        return {
            "items": matched_items,
            "count": len(matched_items),
            "total_items": len(items),
            "field": field,
            "value": value,
            "max_distance": max_distance,
            "as_field": as_field,
        }


__all__ = ["FuzzyExecutor"]
