"""
Idempotency Key Resolution - Runtime resolution of idempotency key templates.

Resolves template strings like "ticket:{{args.title}}:{{context.user_id}}"
using actual values from tool arguments and execution context.
"""
import hashlib
import re
from typing import Any, Optional


# Regex to match template placeholders: {{namespace.path.to.value}}
TEMPLATE_PATTERN = re.compile(r"\{\{(\w+(?:\.\w+)*)\}\}")


class IdempotencyKeyError(Exception):
    """Error resolving idempotency key template."""
    pass


def resolve_idempotency_key(
    template: str,
    args: dict[str, Any],
    context: Optional[dict[str, Any]] = None,
    *,
    hash_long_values: bool = True,
    max_key_length: int = 256,
    strict: bool = False,
) -> str:
    """
    Resolve an idempotency key template to a concrete key.

    Template syntax:
        - {{args.field}} - Access tool argument field
        - {{args.nested.field}} - Access nested argument field
        - {{context.field}} - Access execution context field
        - {{context.user_id}} - Common context fields

    Args:
        template: The idempotency key template string
        args: Tool arguments dictionary
        context: Execution context dictionary (optional)
        hash_long_values: If True, hash values longer than 64 chars (default: True)
        max_key_length: Maximum length of resolved key (default: 256)
        strict: If True, raise error on missing fields; if False, use placeholder (default: False)

    Returns:
        The resolved idempotency key string

    Raises:
        IdempotencyKeyError: If strict=True and a required field is missing

    Example:
        >>> resolve_idempotency_key(
        ...     "ticket:{{args.title}}:{{context.user_id}}",
        ...     args={"title": "Bug Report"},
        ...     context={"user_id": "user_123"}
        ... )
        'ticket:Bug Report:user_123'
    """
    context = context or {}

    # Build lookup namespaces
    namespaces = {
        "args": args,
        "context": context,
    }

    def resolve_placeholder(match: re.Match) -> str:
        """Resolve a single placeholder."""
        path = match.group(1)
        parts = path.split(".")

        if len(parts) < 2:
            if strict:
                raise IdempotencyKeyError(
                    f"Invalid placeholder format: {{{{{path}}}}}. "
                    "Expected format: {{namespace.field}}"
                )
            return f"__{path}__"

        namespace = parts[0]
        field_path = parts[1:]

        if namespace not in namespaces:
            if strict:
                raise IdempotencyKeyError(
                    f"Unknown namespace '{namespace}' in placeholder {{{{{path}}}}}. "
                    f"Valid namespaces: {list(namespaces.keys())}"
                )
            return f"__{path}__"

        # Traverse the nested path
        value = namespaces[namespace]
        for field in field_path:
            if isinstance(value, dict):
                if field in value:
                    value = value[field]
                else:
                    if strict:
                        raise IdempotencyKeyError(
                            f"Field '{field}' not found in {namespace}.{'.'.join(field_path[:field_path.index(field)])}. "
                            f"Available fields: {list(value.keys()) if isinstance(value, dict) else 'N/A'}"
                        )
                    return f"__{path}__"
            else:
                if strict:
                    raise IdempotencyKeyError(
                        f"Cannot access '{field}' on non-dict value at "
                        f"{namespace}.{'.'.join(field_path[:field_path.index(field)])}"
                    )
                return f"__{path}__"

        # Convert value to string
        str_value = _value_to_string(value, hash_long_values)
        return str_value

    # Resolve all placeholders
    resolved = TEMPLATE_PATTERN.sub(resolve_placeholder, template)

    # Truncate if too long
    if len(resolved) > max_key_length:
        # Hash the overflow portion
        hash_suffix = hashlib.sha256(resolved.encode()).hexdigest()[:16]
        resolved = resolved[: max_key_length - 17] + ":" + hash_suffix

    return resolved


def _value_to_string(value: Any, hash_long: bool = True) -> str:
    """Convert a value to a string suitable for idempotency key."""
    if value is None:
        return "null"

    if isinstance(value, bool):
        return "true" if value else "false"

    if isinstance(value, (int, float)):
        return str(value)

    if isinstance(value, str):
        # Hash long strings
        if hash_long and len(value) > 64:
            return hashlib.sha256(value.encode()).hexdigest()[:16]
        # Sanitize: replace problematic characters
        return value.replace(":", "_").replace("/", "_").replace("\\", "_")

    if isinstance(value, (list, tuple)):
        # Hash lists
        import json
        serialized = json.dumps(value, sort_keys=True, default=str)
        if hash_long and len(serialized) > 64:
            return hashlib.sha256(serialized.encode()).hexdigest()[:16]
        return serialized.replace(":", "_")

    if isinstance(value, dict):
        # Hash dicts
        import json
        serialized = json.dumps(value, sort_keys=True, default=str)
        if hash_long and len(serialized) > 64:
            return hashlib.sha256(serialized.encode()).hexdigest()[:16]
        return serialized.replace(":", "_")

    # Fallback: convert to string and sanitize
    str_val = str(value)
    if hash_long and len(str_val) > 64:
        return hashlib.sha256(str_val.encode()).hexdigest()[:16]
    return str_val.replace(":", "_")


def generate_default_idempotency_key(
    tool_name: str,
    args: dict[str, Any],
    context: Optional[dict[str, Any]] = None,
) -> str:
    """
    Generate a default idempotency key when no template is specified.

    Creates a key based on tool name and a hash of arguments.

    Args:
        tool_name: Name of the tool
        args: Tool arguments
        context: Execution context (optional, not used in default key)

    Returns:
        Default idempotency key string
    """
    import json

    # Hash the args
    args_str = json.dumps(args, sort_keys=True, default=str)
    args_hash = hashlib.sha256(args_str.encode()).hexdigest()[:16]

    return f"{tool_name}:{args_hash}"


def validate_idempotency_template(template: str) -> list[str]:
    """
    Validate an idempotency key template.

    Args:
        template: The template string to validate

    Returns:
        List of validation warnings (empty if valid)
    """
    warnings = []

    # Find all placeholders
    placeholders = TEMPLATE_PATTERN.findall(template)

    if not placeholders:
        warnings.append("Template contains no placeholders - key will be static")

    for placeholder in placeholders:
        parts = placeholder.split(".")
        if len(parts) < 2:
            warnings.append(
                f"Invalid placeholder format: {{{{{placeholder}}}}}. "
                "Expected: {{namespace.field}}"
            )
        elif parts[0] not in ("args", "context"):
            warnings.append(
                f"Unknown namespace '{parts[0]}' in {{{{{placeholder}}}}}. "
                "Valid namespaces: args, context"
            )

    return warnings


def resolve_action_args(
    args: dict[str, Any],
    state: dict[str, Any],
    *,
    strict: bool = False,
) -> dict[str, Any]:
    """
    Resolve template placeholders in action args using state from context atoms.

    Template syntax in args values:
        - {{state.key}} - Access state atom by key (e.g., {{state.referral.id}})
        - {{state.nested.key}} - Access nested state path

    Args:
        args: Action args dictionary (may contain template strings as values)
        state: State dictionary built from context atoms (key -> value mapping)
        strict: If True, raise error on missing fields; if False, leave template as-is

    Returns:
        Resolved args dictionary with templates replaced by actual values

    Raises:
        IdempotencyKeyError: If strict=True and a required field is missing

    Example:
        >>> resolve_action_args(
        ...     args={"referral_id": "{{state.referral.id}}", "amount": 100},
        ...     state={"referral.id": "uuid-123"}
        ... )
        {'referral_id': 'uuid-123', 'amount': 100}
    """
    resolved = {}

    for key, value in args.items():
        if isinstance(value, str):
            # Check if value contains template placeholders
            resolved[key] = _resolve_template_string(value, state, strict)
        elif isinstance(value, dict):
            # Recursively resolve nested dicts
            resolved[key] = resolve_action_args(value, state, strict=strict)
        elif isinstance(value, list):
            # Resolve templates in list items
            resolved[key] = [
                _resolve_template_string(item, state, strict) if isinstance(item, str)
                else resolve_action_args(item, state, strict=strict) if isinstance(item, dict)
                else item
                for item in value
            ]
        else:
            resolved[key] = value

    return resolved


def _resolve_template_string(
    value: str,
    state: dict[str, Any],
    strict: bool = False,
) -> Any:
    """Resolve template placeholders in a string value."""
    # Check if the entire value is a single template (return typed value)
    single_match = re.fullmatch(r"\{\{state\.(.+?)\}\}", value)
    if single_match:
        state_key = single_match.group(1)
        if state_key in state:
            return state[state_key]
        elif strict:
            raise IdempotencyKeyError(
                f"State key '{state_key}' not found. "
                f"Available keys: {list(state.keys())}"
            )
        else:
            # Leave template as-is if not found (non-strict mode)
            return value

    # Handle multiple templates or templates embedded in strings
    def replace_match(match: re.Match) -> str:
        path = match.group(1)
        parts = path.split(".", 1)

        if len(parts) < 2 or parts[0] != "state":
            if strict:
                raise IdempotencyKeyError(
                    f"Invalid template format: {{{{{path}}}}}. "
                    "Expected format: {{state.key}}"
                )
            return match.group(0)  # Leave as-is

        state_key = parts[1]
        if state_key in state:
            return str(state[state_key])
        elif strict:
            raise IdempotencyKeyError(
                f"State key '{state_key}' not found. "
                f"Available keys: {list(state.keys())}"
            )
        else:
            return match.group(0)  # Leave as-is

    return TEMPLATE_PATTERN.sub(replace_match, value)


def atoms_to_state_dict(atoms: list) -> dict[str, Any]:
    """
    Convert context atoms to a state dictionary for template resolution.

    Args:
        atoms: List of atom objects (StateAtom, TagAtom, EventAtom)

    Returns:
        Dictionary mapping atom keys to their values

    Example:
        >>> from memrail.atoms import state
        >>> atoms = [state("user.id", "U-123"), state("referral.id", "ref-456")]
        >>> atoms_to_state_dict(atoms)
        {'user.id': 'U-123', 'referral.id': 'ref-456'}
    """
    state_dict = {}

    for atom in atoms:
        # Handle both dataclass atoms and dict atoms
        if hasattr(atom, "key") and hasattr(atom, "value"):
            # Dataclass-style atom
            state_dict[atom.key] = atom.value
        elif isinstance(atom, dict):
            # Dict-style atom
            key = atom.get("key")
            value = atom.get("value")
            if key is not None:
                state_dict[key] = value

    return state_dict
