"""Resolve immutable execution contracts from raw execution intent.

This module is the single precedence path for effective runtime truth.
It is intentionally pure: no filesystem, subprocess, network, or environment I/O.
"""

from __future__ import annotations

import hashlib
import json
from typing import Literal

from deal import post, pre

from anima.core.contracts import (
    ContractIdentity,
    ContractResolverProvenance,
    ContractSourceHashes,
    PersonaProvenance,
    PersonaSpawnability,
    RuntimeModelContext,
    RuntimeOutputPolicy,
    RuntimeToolAccess,
    SpawnabilityReason,
)
from anima.core.execution.contracts import (
    ExecutionIntent,
    ResolvedExecutionContract,
    ResolvedLifecyclePolicy,
    ResolvedRuntime,
)
from anima.core.lifecycle import resolve_lifecycle_completion_policy
from anima.core.model_precedence import resolve_model_precedence
from anima.core.schemas.enums import LifecycleType, SideEffectPolicy
from anima.core.tool_resolver import resolve_effective_tool_names


@post(lambda result: len(result) > 0)
def _stable_json(value: object) -> str:
    """Return stable JSON for deterministic hashing.

    >>> _stable_json({"b": 2, "a": 1})
    '{"a":1,"b":2}'
    """
    return json.dumps(value, sort_keys=True, separators=(",", ":"), default=str)


@post(lambda result: result.startswith("sha256:"))
def _hash_fragment(value: object) -> str:
    """Compute deterministic sha256 fragment with explicit prefix.

    >>> _hash_fragment({"x": 1}).startswith("sha256:")
    True
    """
    digest = hashlib.sha256(_stable_json(value).encode("utf-8")).hexdigest()
    return f"sha256:{digest}"


@post(lambda result: result is None or len(result) > 0)
def _normalize_runtime_path(path_value: str | None) -> str | None:
    """Normalize runtime path values to absolute paths.

    Core resolver keeps path strings stable; shell may normalize externally.

    >>> _normalize_runtime_path(None) is None
    True
    >>> _normalize_runtime_path("/tmp")
    '/tmp'
    >>> _normalize_runtime_path(".") is not None
    True
    """
    if path_value is None:
        return None
    if len(path_value) == 0:
        return None
    return path_value


@post(lambda result: result is None or len(result) > 0)
def _resolved_optional(
    *,
    override_value: str | None,
    override_set: bool,
    fallback_value: str | None,
    default_value: str | None,
) -> str | None:
    """Resolve optional string with explicit-null overwrite semantics.

    >>> _resolved_optional(
    ...     override_value=None,
    ...     override_set=True,
    ...     fallback_value="job",
    ...     default_value="default",
    ... ) is None
    True
    >>> _resolved_optional(
    ...     override_value="cli",
    ...     override_set=True,
    ...     fallback_value="job",
    ...     default_value="default",
    ... )
    'cli'
    >>> _resolved_optional(
    ...     override_value=None,
    ...     override_set=False,
    ...     fallback_value="job",
    ...     default_value="default",
    ... )
    'job'
    """

    def _normalize(value: str | None) -> str | None:
        if value is None:
            return None
        return value if len(value) > 0 else None

    if override_set:
        return _normalize(override_value)
    if fallback_value is not None:
        return _normalize(fallback_value)
    return _normalize(default_value)


@post(lambda result: result is None or result > 0)
def _resolved_positive_int(
    *,
    configured_value: int | None,
    default_value: int | None,
) -> int | None:
    """Resolve optional positive-int output policy caps.

    >>> _resolved_positive_int(configured_value=1200, default_value=None)
    1200
    >>> _resolved_positive_int(configured_value=0, default_value=2048)
    2048
    >>> _resolved_positive_int(configured_value=None, default_value=None) is None
    True
    """
    if isinstance(configured_value, int) and configured_value > 0:
        return configured_value
    if isinstance(default_value, int) and default_value > 0:
        return default_value
    return None


@post(lambda result: result.spawnability is not None)
def _resolved_persona_provenance(
    *,
    source: Literal["file", "inline", "synthesized", "snapshot"],
    reusable_persona_ref: str | None,
    inline_behavior: Literal["auto_materialize_snapshot", "reject_spawn"],
) -> PersonaProvenance:
    """Resolve structured persona provenance and spawnability policy.

    >>> file_provenance = _resolved_persona_provenance(
    ...     source="file",
    ...     reusable_persona_ref="worker@v1",
    ...     inline_behavior="reject_spawn",
    ... )
    >>> file_provenance.spawnability.can_spawn
    True
    >>> inline_blocked = _resolved_persona_provenance(
    ...     source="inline",
    ...     reusable_persona_ref=None,
    ...     inline_behavior="reject_spawn",
    ... )
    >>> inline_blocked.spawnability.can_spawn
    False
    """
    if source in {"file", "snapshot"}:
        if reusable_persona_ref:
            return PersonaProvenance(
                source=source,
                reusable_persona_ref=reusable_persona_ref,
                materialization_status="materialized",
                inline_behavior=inline_behavior,
                child_inheritance_policy=(
                    "inherit_reusable_persona_ref"
                    if source == "file"
                    else "inherit_materialized_snapshot"
                ),
                spawnability=PersonaSpawnability(can_spawn=True),
            )
        return PersonaProvenance(
            source=source,
            reusable_persona_ref=None,
            materialization_status="not_materialized",
            inline_behavior=inline_behavior,
            child_inheritance_policy="require_explicit_child_persona",
            spawnability=PersonaSpawnability(
                can_spawn=False,
                reason=SpawnabilityReason(
                    code="missing_reusable_persona_ref",
                    message="Persona cannot spawn child agents without a reusable reference.",
                    user_action="Provide an explicit child persona or materialize a reusable snapshot.",
                    retryable=True,
                    details={"source": source},
                ),
            ),
        )

    # Inline/synthesized personas must resolve to one explicit behavior.
    if inline_behavior == "auto_materialize_snapshot" and reusable_persona_ref:
        return PersonaProvenance(
            source=source,
            reusable_persona_ref=reusable_persona_ref,
            materialization_status="materialized",
            inline_behavior=inline_behavior,
            child_inheritance_policy="inherit_materialized_snapshot",
            spawnability=PersonaSpawnability(can_spawn=True),
        )

    reason_code = (
        "inline_persona_requires_materialization"
        if source == "inline"
        else "synthesized_persona_requires_materialization"
    )
    return PersonaProvenance(
        source=source,
        reusable_persona_ref=None,
        materialization_status="not_materialized",
        inline_behavior=inline_behavior,
        child_inheritance_policy="reject_child_inheritance",
        spawnability=PersonaSpawnability(
            can_spawn=False,
            reason=SpawnabilityReason(
                code=reason_code,
                message=(
                    "Persona cannot spawn child agents until materialized into a reusable snapshot."
                ),
                user_action=(
                    "Retry with a reusable persona snapshot or provide an explicit child persona."
                ),
                retryable=True,
                details={"source": source},
            ),
        ),
    )


@post(lambda result: len(result.identity.instance_id) > 0)
@post(lambda result: len(result.runtime.effective_model) > 0)
@pre(
    lambda intent, **kwargs: (
        (
            intent.overrides.requested_model is not None
            and len(intent.overrides.requested_model) > 0
        )
        or (
            intent.defaults.default_model is not None
            and len(intent.defaults.default_model) > 0
        )
        or (
            intent.persona_spec is not None
            and intent.persona_spec.model is not None
            and len(intent.persona_spec.model) > 0
        )
    )
)
def resolve_execution_contract(
    intent: ExecutionIntent,
    *,
    resolver_version: str = "resolver/0.1.0",
    persona_source: Literal["file", "inline", "synthesized", "snapshot"] = "file",
    inline_behavior: Literal[
        "auto_materialize_snapshot", "reject_spawn"
    ] = "reject_spawn",
) -> ResolvedExecutionContract:
    """Resolve an immutable execution contract from requested execution intent.

    Spec source:
    - docs/proposals/execution-architecture-refactor.md#summary
    - docs/proposals/execution-architecture-refactor.md#data-model-boundaries/resolvedexecutioncontract
    - docs/proposals/execution-architecture-refactor.md#execution-contract-specific-design-rules/tool-access-semantics
    - docs/proposals/execution-architecture-refactor.md#execution-contract-specific-design-rules/persona-provenance-and-subagent-inheritance
    - docs/proposals/execution-architecture-refactor.md#execution-contract-specific-design-rules/lifecycle-semantics

    >>> from anima.core.execution import ExecutionIntentOverrides
    >>> from anima.core.schemas.models import JobSpec, PersonaSpec
    >>> intent = ExecutionIntent(
    ...     persona_spec=PersonaSpec(
    ...         id="worker",
    ...         description="Worker",
    ...         prompt="Do work",
    ...         model="gpt-4o-mini",
    ...         tools={"read_file": "read_only"},
    ...     ),
    ...     job_spec=JobSpec(persona_ref="worker@v1", instance_id="inst_1"),
    ...     overrides=ExecutionIntentOverrides(
    ...         requested_model="gpt-4o",
    ...         requested_cli_tools=("write_file",),
    ...     ),
    ... )
    >>> contract = resolve_execution_contract(intent)
    >>> contract.runtime.effective_model
    'gpt-4o'
    >>> contract.tool_access.effective_tools
    ('write_file',)
    """
    persona = intent.persona_spec
    job = intent.job_spec
    overrides = intent.overrides
    defaults = intent.defaults

    instance_id = job.instance_id if job is not None else "unknown-instance"

    effective_model = resolve_model_precedence(
        cli_model=overrides.requested_model,
        env_model=None,
        config_model=defaults.default_model,
        persona_model=persona.model if persona is not None else None,
    )
    if effective_model is None:
        raise ValueError("Unable to resolve effective model from execution intent")

    requested_timeout_set = "requested_timeout" in overrides.model_fields_set
    requested_work_dir_set = "requested_work_dir" in overrides.model_fields_set
    requested_project_root_set = "requested_project_root" in overrides.model_fields_set
    requested_filesystem_scope_set = (
        "requested_filesystem_scope" in overrides.model_fields_set
    )

    timeout = _resolved_optional(
        override_value=overrides.requested_timeout,
        override_set=requested_timeout_set,
        fallback_value=job.timeout if job is not None else None,
        default_value=defaults.default_timeout,
    )
    work_dir = _normalize_runtime_path(
        _resolved_optional(
            override_value=overrides.requested_work_dir,
            override_set=requested_work_dir_set,
            fallback_value=None,
            default_value=defaults.default_work_dir,
        )
    )
    project_root = _normalize_runtime_path(
        _resolved_optional(
            override_value=overrides.requested_project_root,
            override_set=requested_project_root_set,
            fallback_value=None,
            default_value=defaults.default_project_root,
        )
    )
    filesystem_scope = _resolved_optional(
        override_value=overrides.requested_filesystem_scope,
        override_set=requested_filesystem_scope_set,
        fallback_value=None,
        default_value=defaults.default_filesystem_scope,
    )

    declared_persona_tools = (
        tuple(name for name in persona.tools.keys() if len(name) > 0)
        if persona is not None and persona.tools
        else ()
    )
    requested_cli_tools = tuple(
        name for name in overrides.requested_cli_tools if len(name) > 0
    )
    requested_mcp_tools = tuple(
        name for name in overrides.requested_mcp_tools if len(name) > 0
    )
    effective_tools = resolve_effective_tool_names(
        declared_persona_tools=declared_persona_tools,
        requested_cli_tools=requested_cli_tools,
        requested_mcp_tools=requested_mcp_tools,
        cli_tools_explicitly_set="requested_cli_tools" in overrides.model_fields_set,
        mcp_tools_explicitly_set="requested_mcp_tools" in overrides.model_fields_set,
    )

    persona_policy = persona.side_effect_policy if persona is not None else None
    effective_side_effect_policy = (
        overrides.requested_side_effect_policy
        or persona_policy
        or defaults.default_side_effect_policy
        or SideEffectPolicy.read_only
    )

    lifecycle, completion = resolve_lifecycle_completion_policy(
        requested_lifecycle=overrides.requested_lifecycle,
        job_lifecycle=job.lifecycle if job is not None else None,
        requested_completion_policy=overrides.requested_completion_policy,
        job_completion_policy=job.completion_policy if job is not None else None,
    )

    reusable_persona_ref = job.persona_ref if job is not None else None
    provenance = _resolved_persona_provenance(
        source=persona_source,
        reusable_persona_ref=reusable_persona_ref,
        inline_behavior=inline_behavior,
    )

    source_hashes = ContractSourceHashes(
        persona_digest=_hash_fragment(persona.model_dump(mode="json"))
        if persona is not None
        else None,
        job_digest=_hash_fragment(job.model_dump(mode="json"))
        if job is not None
        else None,
        cli_digest=_hash_fragment(overrides.model_dump(mode="json")),
        defaults_digest=_hash_fragment(defaults.model_dump(mode="json")),
    )

    output_provider_profile = defaults.default_output_provider_profile
    if output_provider_profile is None or len(output_provider_profile.strip()) == 0:
        output_provider_profile = "openai-compatible"
    output_truncation_strategy = defaults.default_truncation_strategy
    if (
        output_truncation_strategy is None
        or len(output_truncation_strategy.strip()) == 0
    ):
        output_truncation_strategy = "paginate_then_truncate"
    output_per_call = _resolved_positive_int(
        configured_value=defaults.default_per_call_tool_output_chars,
        default_value=None,
    )
    output_per_turn = _resolved_positive_int(
        configured_value=defaults.default_per_turn_tool_output_chars,
        default_value=None,
    )

    return ResolvedExecutionContract(
        identity=ContractIdentity(
            instance_id=instance_id,
            resolver=ContractResolverProvenance(
                resolver_version=resolver_version,
                source_hashes=source_hashes,
            ),
        ),
        runtime=ResolvedRuntime(
            effective_model=effective_model,
            timeout=timeout,
            max_turns=job.max_turns if job is not None else None,
            max_tokens=job.max_tokens if job is not None else None,
            work_dir=work_dir,
            project_root=project_root,
            filesystem_scope=filesystem_scope,
            sandbox_posture=effective_side_effect_policy.value,
        ),
        tool_access=RuntimeToolAccess(
            declared_persona_tools=declared_persona_tools,
            requested_cli_tools=requested_cli_tools,
            requested_mcp_tools=requested_mcp_tools,
            effective_tools=effective_tools,
            effective_presets=(),
            effective_side_effect_policy=effective_side_effect_policy,
        ),
        persona_provenance=provenance,
        lifecycle_policy=ResolvedLifecyclePolicy(
            lifecycle=lifecycle,
            completion_policy=completion,
            attachable=lifecycle == LifecycleType.daemon,
        ),
        output_policy=RuntimeOutputPolicy(
            provider_profile=output_provider_profile,
            per_call_tool_output_chars=output_per_call,
            per_turn_tool_output_chars=output_per_turn,
            truncation_strategy=output_truncation_strategy,
        ),
        model_context=RuntimeModelContext(
            values={
                "instance_id": instance_id,
                "effective_model": effective_model,
                "lifecycle": lifecycle.value,
                "completion_policy": completion.value,
                "spawnable": provenance.spawnability.can_spawn,
                # Model-visible runtime context per spec
                # docs/proposals/execution-architecture-refactor.md#model-visible-runtime-context
                "work_dir": work_dir,
                "project_root": project_root,
                "filesystem_scope": filesystem_scope,
                "effective_tool_count": len(effective_tools),
            }
        ),
    )


__all__ = ["resolve_execution_contract"]
