from __future__ import annotations

import hashlib
import json
from typing import Any, Dict, Mapping, Optional, Tuple


def _canonical_json(value: Any) -> bytes:
    return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")


def _hash_json(value: Any) -> str:
    return hashlib.sha256(_canonical_json(value)).hexdigest()


def _flatten(prefix: str, value: Any, out: Dict[str, Any]) -> None:
    if isinstance(value, Mapping):
        for key in sorted(value.keys(), key=str):
            child = value[key]
            next_prefix = f"{prefix}.{key}" if prefix else str(key)
            _flatten(next_prefix, child, out)
        return
    if isinstance(value, (list, tuple)):
        for idx, child in enumerate(value):
            next_prefix = f"{prefix}[{idx}]"
            _flatten(next_prefix, child, out)
        return
    out[prefix or "value"] = value


def _normalize_context_order(context_order: Any, args: Dict[str, Any]) -> str:
    if isinstance(context_order, str):
        return context_order.strip()
    if isinstance(context_order, (list, tuple)):
        return "->".join(str(item) for item in context_order)
    if isinstance(context_order, Mapping):
        return "->".join(str(key) for key in context_order.keys())
    if isinstance(args, Mapping):
        return "->".join(str(key) for key in args.keys())
    return ""


def _default_identity_fields(args: Dict[str, Any]) -> Dict[str, Any]:
    if not isinstance(args, Mapping):
        return {}
    out: Dict[str, Any] = {}
    preferred = {
        "symbol",
        "instrument",
        "side",
        "action",
        "qty",
        "quantity",
        "amount",
        "amount_cents",
        "currency",
        "account_id",
        "position_id",
        "trade_id",
        "approval_gate_id",
    }
    for key, value in args.items():
        if key == "id" or key.endswith("_id") or key in preferred:
            out[str(key)] = value
    return out


def build_intent_key(tool_name: str, args: Dict[str, Any], identity_fields: Optional[Dict[str, Any]] = None) -> Tuple[str, Dict[str, Any]]:
    fields = identity_fields or {}
    if not fields:
        fields = _default_identity_fields(args)
    if not fields:
        if isinstance(args, Mapping):
            # Fallback to full args hash to avoid cross-run collisions on
            # tool calls that use generic arg names like {"request": "..."}.
            fields = {"args_hash": "sha256:" + _hash_json(args)}
        else:
            fields = {"scope": "tool_call"}
    key_material = {"tool": tool_name, "identity": fields}
    return "intent:" + _hash_json(key_material), fields


def build_intent_fingerprint(
    *,
    tool_name: str,
    args: Dict[str, Any],
    policy_version: str,
    dependency_signature: str,
    semantic_fields: Any,
    context_order: Any,
) -> Tuple[str, Dict[str, str], str]:
    flattened: Dict[str, Any] = {}
    source = semantic_fields if semantic_fields is not None else args
    _flatten("", source, flattened)
    field_hashes = {path: _hash_json(value) for path, value in flattened.items()}
    context_order_sig = _normalize_context_order(context_order, args)
    fingerprint_material = {
        "tool": tool_name,
        "policy_version": policy_version or "",
        "dependency_signature": dependency_signature or "",
        "field_semantic_hashes": field_hashes,
        "context_ordering_signature": context_order_sig,
    }
    return _hash_json(fingerprint_material), field_hashes, context_order_sig


def diff_field_hashes(previous: Dict[str, str], current: Dict[str, str]) -> list[str]:
    keys = set(previous.keys()) | set(current.keys())
    return [key for key in sorted(keys) if previous.get(key) != current.get(key)]
