"""Configuration-driven query and answer evidence contracts."""

from __future__ import annotations

import ast
from decimal import Decimal, InvalidOperation
from functools import lru_cache
import json
import os
from pathlib import Path
import re
from typing import Any, Mapping


ROOT = Path(__file__).resolve().parents[1]
DEFAULT_CONTRACT_FILE = ROOT / "config" / "hmm_hr_query_contracts.json"


def _contract_file() -> Path:
    configured = str(
        os.getenv("AI_WEB_AGENT_CONSOLE_QUERY_CONTRACTS_PATH")
        or os.getenv("POC4_QUERY_CONTRACTS_PATH")
        or ""
    ).strip()
    if not configured:
        return DEFAULT_CONTRACT_FILE
    path = Path(configured).expanduser()
    return path if path.is_absolute() else ROOT / path


@lru_cache(maxsize=4)
def _load_contract_file(path_text: str) -> tuple[Mapping[str, Any], ...]:
    path = Path(path_text)
    if not path.exists():
        return ()
    value = json.loads(path.read_text(encoding="utf-8"))
    contracts = value.get("contracts") if isinstance(value, Mapping) else None
    if not isinstance(contracts, list):
        raise ValueError("query contracts must contain a contracts array")
    return tuple(item for item in contracts if isinstance(item, Mapping))


def load_query_contracts() -> tuple[Mapping[str, Any], ...]:
    return _load_contract_file(str(_contract_file()))


def matching_query_contracts(
    question: str,
    tool_name: str,
) -> tuple[Mapping[str, Any], ...]:
    normalized_question = " ".join(str(question or "").casefold().split())
    normalized_tool = str(tool_name or "").strip()
    selected: list[Mapping[str, Any]] = []
    for contract in load_query_contracts():
        tools = contract.get("applies_to_tools")
        if isinstance(tools, list) and normalized_tool not in {
            str(item) for item in tools
        }:
            continue
        intent = contract.get("intent_match")
        if not isinstance(intent, Mapping):
            continue
        subject_terms = [
            str(item).casefold()
            for item in intent.get("subject_terms_any", [])
            if str(item).strip()
        ]
        action_terms = [
            str(item).casefold()
            for item in intent.get("action_terms_any", [])
            if str(item).strip()
        ]
        if subject_terms and not any(
            term in normalized_question for term in subject_terms
        ):
            continue
        if action_terms and not any(
            term in normalized_question for term in action_terms
        ):
            continue
        selected.append(contract)
    return tuple(selected)


def query_contract_guidance(question: str, tool_name: str) -> str:
    contracts = matching_query_contracts(question, tool_name)
    if not contracts:
        return ""
    return json.dumps(
        {"query_contracts": contracts},
        ensure_ascii=False,
        separators=(",", ":"),
    )


def append_query_contract_guidance(
    tool_query: str,
    *,
    original_question: str,
    tool_name: str,
) -> str:
    guidance = query_contract_guidance(original_question, tool_name)
    normalized = str(tool_query or "").strip()
    if not guidance:
        return normalized
    return (
        f"{normalized}\n"
        "다음 질의 계약의 필드·계산·시간 기준을 반드시 지켜 결과를 반환하세요. "
        f"계약: {guidance}"
    )


def _json_data_result(value: str) -> Any:
    marker = "DATA_RESULT"
    position = value.find(marker)
    if position < 0:
        return None
    remainder = value[position + len(marker) :]
    object_position = remainder.find("{")
    array_position = remainder.find("[")
    positions = [
        candidate
        for candidate in (object_position, array_position)
        if candidate >= 0
    ]
    if not positions:
        return None
    candidate = remainder[min(positions) :]
    try:
        parsed, _ = json.JSONDecoder().raw_decode(candidate)
    except (TypeError, ValueError, json.JSONDecodeError):
        return None
    return parsed


def _evidence_rows(value: Any) -> list[Mapping[str, Any]]:
    rows: list[Mapping[str, Any]] = []
    if isinstance(value, Mapping):
        rows.append(value)
        for item in value.values():
            rows.extend(_evidence_rows(item))
    elif isinstance(value, list):
        for item in value:
            rows.extend(_evidence_rows(item))
    elif isinstance(value, str):
        parsed = _json_data_result(value)
        if parsed is not None:
            rows.extend(_evidence_rows(parsed))
    return rows


def _decimal(value: Any) -> Decimal:
    if isinstance(value, bool) or value is None:
        raise InvalidOperation
    return Decimal(str(value))


def _evaluate_contract_expression(
    expression: str,
    row: Mapping[str, Any],
) -> Decimal:
    tree = ast.parse(expression, mode="eval")

    def evaluate(node: ast.AST) -> Decimal:
        if isinstance(node, ast.Expression):
            return evaluate(node.body)
        if isinstance(node, ast.Name):
            key = node.id.casefold()
            if key not in row:
                raise InvalidOperation
            return _decimal(row[key])
        if isinstance(node, ast.Constant):
            return _decimal(node.value)
        if isinstance(node, ast.UnaryOp) and isinstance(
            node.op, (ast.UAdd, ast.USub)
        ):
            value = evaluate(node.operand)
            return value if isinstance(node.op, ast.UAdd) else -value
        if isinstance(node, ast.BinOp) and isinstance(
            node.op, (ast.Add, ast.Sub, ast.Mult, ast.Div)
        ):
            left = evaluate(node.left)
            right = evaluate(node.right)
            if isinstance(node.op, ast.Add):
                return left + right
            if isinstance(node.op, ast.Sub):
                return left - right
            if isinstance(node.op, ast.Mult):
                return left * right
            return left / right
        raise ValueError("unsupported contract expression")

    return evaluate(tree)


def evidence_contract_report(
    contracts: tuple[Mapping[str, Any], ...],
    evidence: Any,
) -> list[dict[str, Any]]:
    rows = [
        {str(key).strip().casefold(): value for key, value in row.items()}
        for row in _evidence_rows(evidence)
    ]
    reports: list[dict[str, Any]] = []
    for contract in contracts:
        data_contract = contract.get("data_contract")
        if not isinstance(data_contract, Mapping):
            continue
        required = [
            str(item).strip().casefold()
            for item in data_contract.get("required_fields", [])
            if str(item).strip()
        ]
        matching_row = next(
            (row for row in rows if required and all(key in row for key in required)),
            None,
        )
        observed = sorted(
            {
                key
                for row in rows
                for key in row
                if not required or key in required
            }
        )
        missing = [
            key.upper()
            for key in required
            if matching_row is None or key not in matching_row
        ]
        computed_checks: list[dict[str, Any]] = []
        computed_fields = data_contract.get("computed_fields")
        if matching_row is not None and isinstance(computed_fields, Mapping):
            for field, definition in computed_fields.items():
                normalized_field = str(field).strip().casefold()
                expression = (
                    str(definition.get("expression") or "").strip()
                    if isinstance(definition, Mapping)
                    else ""
                )
                if not normalized_field or not expression:
                    continue
                try:
                    actual = _decimal(matching_row.get(normalized_field))
                    expected = _evaluate_contract_expression(
                        expression,
                        matching_row,
                    )
                    matches = actual == expected
                    computed_checks.append(
                        {
                            "field": normalized_field.upper(),
                            "expression": expression,
                            "actual": str(actual),
                            "expected": str(expected),
                            "satisfied": matches,
                        }
                    )
                except (InvalidOperation, ValueError, ZeroDivisionError):
                    computed_checks.append(
                        {
                            "field": normalized_field.upper(),
                            "expression": expression,
                            "satisfied": False,
                        }
                    )
        calculations_satisfied = all(
            bool(check.get("satisfied")) for check in computed_checks
        )
        temporal_checks: list[dict[str, Any]] = []
        temporal_contract = contract.get("temporal_contract")
        if matching_row is not None and isinstance(temporal_contract, Mapping):
            status_fields = [
                str(item).strip().casefold()
                for item in temporal_contract.get("period_status_fields", [])
                if str(item).strip()
            ]
            past_status = str(
                temporal_contract.get("past_period_status") or ""
            ).strip()
            observed_status = next(
                (
                    str(matching_row.get(field) or "").strip()
                    for field in status_fields
                    if str(matching_row.get(field) or "").strip()
                ),
                "",
            )
            if observed_status == past_status and past_status:
                decision_field = str(
                    temporal_contract.get("past_period_decision_field") or ""
                ).strip().casefold()
                decision_value = str(
                    temporal_contract.get("past_period_decision_value") or ""
                ).strip()
                actual_decision = str(
                    matching_row.get(decision_field) or ""
                ).strip()
                temporal_checks.append(
                    {
                        "check": "past_period_decision",
                        "field": decision_field.upper(),
                        "expected": decision_value,
                        "actual": actual_decision,
                        "satisfied": bool(
                            decision_field
                            and decision_value
                            and actual_decision == decision_value
                        ),
                    }
                )
                forbidden_patterns = [
                    str(item).strip()
                    for item in temporal_contract.get(
                        "forbidden_past_period_decision_field_patterns", []
                    )
                    if str(item).strip()
                ]
                forbidden_fields = sorted(
                    key.upper()
                    for key in matching_row
                    if any(
                        re.search(pattern, key, flags=re.IGNORECASE)
                        for pattern in forbidden_patterns
                    )
                )
                temporal_checks.append(
                    {
                        "check": "no_past_period_yes_no_decision",
                        "forbidden_fields": forbidden_fields,
                        "satisfied": not forbidden_fields,
                    }
                )
                required_counts = [
                    str(item).strip().casefold()
                    for item in temporal_contract.get(
                        "validate_stated_days_against", []
                    )
                    if str(item).strip()
                ]
                temporal_checks.append(
                    {
                        "check": "date_range_counts",
                        "required_fields": [
                            field.upper() for field in required_counts
                        ],
                        "missing_fields": [
                            field.upper()
                            for field in required_counts
                            if field not in matching_row
                        ],
                        "satisfied": all(
                            field in matching_row for field in required_counts
                        ),
                    }
                )
        temporal_satisfied = all(
            bool(check.get("satisfied")) for check in temporal_checks
        )
        reports.append(
            {
                "contract_id": str(contract.get("id") or ""),
                "satisfied": bool(
                    matching_row is not None
                    and not missing
                    and calculations_satisfied
                    and temporal_satisfied
                ),
                "required_fields": [key.upper() for key in required],
                "observed_required_fields": [key.upper() for key in observed],
                "missing_fields": missing,
                "computed_field_checks": computed_checks,
                "temporal_contract_checks": temporal_checks,
                "validated_record": (
                    {
                        key.upper(): matching_row.get(key)
                        for key in required
                    }
                    if matching_row is not None
                    else {}
                ),
            }
        )
    return reports


def missing_evidence_message(
    contracts: tuple[Mapping[str, Any], ...],
) -> str:
    for contract in contracts:
        answer_contract = contract.get("answer_contract")
        if not isinstance(answer_contract, Mapping):
            continue
        message = str(answer_contract.get("missing_evidence_message") or "").strip()
        if message:
            return message
    return "조회 결과가 답변 계약의 필수 근거를 충족하지 않아 결론을 제공할 수 없습니다."


__all__ = [
    "append_query_contract_guidance",
    "evidence_contract_report",
    "load_query_contracts",
    "matching_query_contracts",
    "missing_evidence_message",
    "query_contract_guidance",
]
