# Copyright 2024 Klira AI
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""FastRulesEngine — single evaluation layer for guardrails.

Handles all pattern matching: regex, domain keywords, fuzzy matching (85%).
No confidence levels — policy action determines behavior.
"""

from __future__ import annotations

import logging
import re
from functools import lru_cache
from typing import Sequence

from klira.sdk.types import Policy, PolicyAction, PolicyMatch

logger = logging.getLogger(__name__)

FUZZY_MATCH_THRESHOLD = 0.85

try:
    import re2 as _re_engine  # type: ignore[import-not-found]

    _RE2_AVAILABLE = True
except ImportError:
    _re_engine = re
    _RE2_AVAILABLE = False


_RE2_WARNING_EMITTED = False


@lru_cache(maxsize=1000)
def _compile_pattern(pattern: str) -> re.Pattern[str]:
    """Compile and cache a regex pattern."""
    global _RE2_WARNING_EMITTED
    if _RE2_AVAILABLE:
        return _re_engine.compile(f"(?i){pattern}")
    if not _RE2_WARNING_EMITTED:
        _RE2_WARNING_EMITTED = True
        logger.warning(
            "google-re2 not installed — regex patterns are vulnerable to ReDoS. "
            "Install with: pip install 'klira[security]'"
        )
    return re.compile(pattern, re.IGNORECASE)


def _fuzzy_similarity(a: str, b: str) -> float:
    """Sequence-aware string similarity using rapidfuzz."""
    if not a or not b:
        return 0.0
    from rapidfuzz import fuzz

    return fuzz.ratio(a.lower(), b.lower(), score_cutoff=0) / 100.0


class FastRulesEngine:
    """Single evaluation layer for all guardrails pattern matching.

    Checks policies against input/output text using:
    1. Regex patterns
    2. Domain keywords
    3. Fuzzy matching (85% similarity threshold)

    No confidence levels — policy action determines behavior directly.
    """

    def evaluate(
        self,
        text: str,
        policies: Sequence[Policy],
        domain: str | None = None,
    ) -> list[PolicyMatch]:
        """Evaluate text against all policies.

        Returns a list of PolicyMatch for each matched policy.
        """
        matches: list[PolicyMatch] = []

        for policy in policies:
            match = self._check_policy(text, policy, domain)
            if match is not None:
                matches.append(match)

        return matches

    def _check_policy(
        self,
        text: str,
        policy: Policy,
        domain: str | None = None,
    ) -> PolicyMatch | None:
        """Check a single policy against text. Returns PolicyMatch or None."""
        # 1. Check regex patterns
        for pattern in policy.patterns:
            try:
                compiled = _compile_pattern(pattern)
                if compiled.search(text):
                    return PolicyMatch(
                        policy=policy,
                        matched_pattern=pattern,
                        match_score=1.0,
                    )
            except (re.error, TypeError, ValueError, RuntimeError):
                logger.warning(
                    "Invalid regex pattern '%s' in policy %s", pattern, policy.id
                )
                continue

        # 2. Check domain keywords
        if domain and policy.domains:
            for policy_domain in policy.domains:
                if policy_domain.lower() in domain.lower():
                    return PolicyMatch(
                        policy=policy,
                        matched_domain=policy_domain,
                        match_score=1.0,
                    )

        # 3. Check fuzzy matching against patterns (treated as keywords)
        for pattern in policy.patterns:
            # Check if pattern appears as fuzzy match in text words
            words = text.split()
            for word in words:
                score = _fuzzy_similarity(word, pattern)
                if score >= FUZZY_MATCH_THRESHOLD:
                    return PolicyMatch(
                        policy=policy,
                        matched_pattern=pattern,
                        match_score=score,
                    )

        return None

    def get_blocking_policies(self, matches: list[PolicyMatch]) -> list[PolicyMatch]:
        """Filter matches to only blocking policies."""
        return [m for m in matches if m.policy.action == PolicyAction.BLOCK]

    def get_augmentation_policies(
        self, matches: list[PolicyMatch]
    ) -> list[PolicyMatch]:
        """Filter matches to only allow/augmentation policies."""
        return [m for m in matches if m.policy.action == PolicyAction.ALLOW]
