"""Contract enforcement: the law of the agent.

CONTRACT.md contains a YAML permissions block that is the single source of
truth for what the agent may and may not do. This module:

1. Parses the YAML block from CONTRACT.md
2. Provides a single check() function — every action goes through it
3. Re-reads CONTRACT.md on file change (no stale cache)
4. Default-deny: if an action isn't explicitly allowed, it's blocked
5. Logs every check to the audit trail

No hardcoded rules. Everything comes from CONTRACT.md.
"""

from __future__ import annotations

import logging
import re
from dataclasses import dataclass, field
from pathlib import Path

import yaml

log = logging.getLogger(__name__)

# Sentinel for "not found in contract"
_NOT_FOUND = object()


@dataclass
class ActionResult:
    """Result of checking an action against the contract."""

    action: str
    allowed: bool
    requires_confirmation: bool = False
    reason: str = ""


@dataclass
class ContractRules:
    """Parsed contract permissions."""

    default: str = "deny"  # "deny" | "allow"
    allow: set[str] = field(default_factory=set)
    confirm: set[str] = field(default_factory=set)
    block: set[str] = field(default_factory=set)
    overrides: dict[str, str] = field(default_factory=dict)
    gated_dirs: list[str] = field(default_factory=list)
    proactivity: str = "advisor"
    privacy_mode: str = "cloud"
    coaching: bool = True
    autopilot: bool = False


class ContractEnforcer:
    """Parses CONTRACT.md and enforces permissions on every agent action.

    Usage:
        enforcer = ContractEnforcer(vault_dir)
        result = enforcer.check("send_email")
        if not result.allowed:
            raise PermissionError(result.reason)
        if result.requires_confirmation:
            # ask user first
    """

    def __init__(self, vault_dir: Path, audit_fn=None, *, paths=None) -> None:  # noqa: ANN001
        self._vault_dir = vault_dir
        self._paths = paths
        if paths:
            identity_path = paths.resolve("system/identity/CONTRACT.md")
        else:
            identity_path = vault_dir / "system" / "identity" / "CONTRACT.md"
        if identity_path.exists():
            self._contract_path = identity_path
        elif (vault_dir / "identity" / "CONTRACT.md").exists():
            self._contract_path = vault_dir / "identity" / "CONTRACT.md"
        else:
            self._contract_path = vault_dir / "CONTRACT.md"
        self._rules: ContractRules | None = None
        self._last_mtime: float = 0
        self._audit_fn = audit_fn  # optional: audit_trail.log_action

    @property
    def rules(self) -> ContractRules:
        """Get current rules, re-parsing if the file changed."""
        self._ensure_fresh()
        if self._rules is None:
            return ContractRules()
        return self._rules

    def check(self, action: str, integration: str | None = None) -> ActionResult:
        """Check if an action is permitted under the current contract.

        This is THE enforcement point. Every action in the system must
        call this before executing.

        Args:
            action: The action identifier (e.g., "send_email", "read_inbox")
            integration: Optional integration name for per-integration overrides

        Returns:
            ActionResult with allowed, requires_confirmation, and reason.
        """
        rules = self.rules

        # 1. Check per-integration override first
        if integration and integration in rules.overrides:
            override = rules.overrides[integration]
            result = self._apply_override(action, integration, override)
            self._audit(action, integration, result)
            return result

        # 2. Check explicit block
        if action in rules.block:
            result = ActionResult(
                action=action, allowed=False,
                reason=f"Blocked by contract: {action}",
            )
            self._audit(action, integration, result)
            self._log_block(action, integration, result)
            return result

        # 3. Check explicit allow
        if action in rules.allow:
            result = ActionResult(action=action, allowed=True)
            self._audit(action, integration, result)
            self._mark_authorized(action)
            return result

        # 4. Check requires confirmation
        if action in rules.confirm:
            result = ActionResult(
                action=action, allowed=True,
                requires_confirmation=True,
                reason=f"Requires confirmation: {action}",
            )
            self._audit(action, integration, result)
            self._mark_authorized(action)
            return result

        # 5. Default policy
        if rules.default == "allow":
            result = ActionResult(action=action, allowed=True)
        else:
            # Default-deny: unknown action is blocked
            result = ActionResult(
                action=action, allowed=False,
                reason=f"Not permitted by contract (default-deny): {action}",
            )

        self._audit(action, integration, result)
        if not result.allowed:
            self._log_block(action, integration, result)

        # Record allowed actions in the current correlation scope's
        # authorized set — the VaultWriter runtime guard reads this to
        # verify that a write is backed by a real contract decision.
        if result.allowed:
            self._mark_authorized(action)

        return result

    def _mark_authorized(self, action: str) -> None:
        """Record an allowed action in the correlation scope's authorized set.

        Import lazily to avoid a hard dependency cycle between
        security.contract and activity.decisions (contract is imported
        very early at startup before activity is wired).
        """
        try:
            from agntspace.activity.decisions import mark_action_authorized
            mark_action_authorized(action)
        except Exception:
            log.debug("mark_action_authorized skipped (decisions unavailable)", exc_info=True)

    def _log_block(self, action: str, integration: str | None, result: object) -> None:
        """Log contract blocks to activity logger."""
        if hasattr(self, "_activity") and self._activity:
            try:
                self._activity.log("contract", "blocked",
                                   f"'{action}'" + (f" for {integration}" if integration else ""),
                                   {"action": action, "integration": integration,
                                    "reason": getattr(result, "reason", "")},
                                   importance="high")
            except Exception:
                pass

    def check_vault_access(self, path: str) -> ActionResult:
        """Check if accessing a vault path is allowed (cloud LLM gate)."""
        rules = self.rules
        if rules.privacy_mode == "local":
            return ActionResult(action="vault_access", allowed=True)

        for gated_dir in rules.gated_dirs:
            if path.startswith(f"{gated_dir}/") or path == gated_dir:
                return ActionResult(
                    action="vault_access", allowed=True,
                    requires_confirmation=True,
                    reason=f"Gated directory: {gated_dir}/ requires confirmation before cloud LLM",
                )
        return ActionResult(action="vault_access", allowed=True)

    def check_proactivity(self, action: str) -> ActionResult:
        """Check if proactivity level allows an autonomous action."""
        rules = self.rules
        tier = rules.proactivity.lower()

        if tier == "observer":
            return ActionResult(
                action=action, allowed=False,
                reason="Observer mode: agent cannot act proactively",
            )
        if tier == "advisor":
            return ActionResult(
                action=action, allowed=True,
                requires_confirmation=True,
                reason="Advisor mode: suggest but confirm before acting",
            )
        if tier == "assistant":
            # Assistant can act on routine, asks for non-routine
            routine = {"journal_write", "log_observation", "heartbeat", "daily_briefing"}
            if action in routine:
                return ActionResult(action=action, allowed=True)
            return ActionResult(
                action=action, allowed=True,
                requires_confirmation=True,
                reason="Assistant mode: non-routine action requires confirmation",
            )
        # Partner: full autonomy within contract boundaries
        return ActionResult(action=action, allowed=True)

    def reload(self) -> ContractRules:
        """Force re-parse of CONTRACT.md."""
        self._rules = self._parse()
        return self._rules

    # ---- Internal ----

    def _ensure_fresh(self) -> None:
        """Re-parse if CONTRACT.md was modified since last read."""
        if not self._contract_path.exists():
            if self._rules is None:
                self._rules = ContractRules()
            return

        mtime = self._contract_path.stat().st_mtime
        if mtime != self._last_mtime:
            self._rules = self._parse()
            self._last_mtime = mtime
            log.info("Contract reloaded from %s", self._contract_path)

    def _parse(self) -> ContractRules:
        """Parse CONTRACT.md — extract YAML permissions block."""
        if not self._contract_path.exists():
            log.warning("CONTRACT.md not found — using default-deny")
            return ContractRules()

        content = self._contract_path.read_text(encoding="utf-8")
        rules = ContractRules()

        # Extract YAML block from markdown (```yaml ... ```)
        yaml_block = self._extract_yaml(content)
        if yaml_block:
            try:
                data = yaml.safe_load(yaml_block)
                if isinstance(data, dict):
                    rules.default = data.get("default") or "deny"
                    rules.allow = set(data.get("allow") or [])
                    rules.confirm = set(data.get("confirm") or [])
                    rules.block = set(data.get("block") or [])
                    rules.overrides = data.get("overrides") or {}
                    rules.gated_dirs = data.get("gated_dirs") or []
            except yaml.YAMLError:
                log.error("Failed to parse YAML permissions in CONTRACT.md")

        # Parse YAML frontmatter (between --- markers) for proactivity/privacy
        frontmatter: dict = {}
        fm_match = re.match(r"^---\s*\n(.*?)\n---", content, flags=re.DOTALL)
        if fm_match:
            try:
                frontmatter = yaml.safe_load(fm_match.group(1)) or {}
            except yaml.YAMLError:
                pass

        # Also parse markdown sections for proactivity, privacy, etc.
        # Frontmatter `proactivity:` wins (supports "custom" + named tiers).
        # Falls back to scanning markdown body for named tiers.
        lower = content.lower()

        fm_proactivity = frontmatter.get("proactivity")
        if fm_proactivity and fm_proactivity in ("observer", "advisor", "assistant", "partner", "custom"):
            rules.proactivity = fm_proactivity
        else:
            for tier in ("observer", "advisor", "assistant", "partner"):
                if re.search(rf"proactivity.*?:\s*\**{tier}\**", lower):
                    rules.proactivity = tier
                    break

        mode_match = re.search(r"\*\*mode\*\*:\s*(\w+)", lower)
        if mode_match:
            parsed_mode = mode_match.group(1)
            if parsed_mode in ("cloud", "local"):
                rules.privacy_mode = parsed_mode

        if re.search(r"coaching:\s*off", lower):
            rules.coaching = False

        if re.search(r"autopilot.*:\s*on", lower):
            rules.autopilot = True

        log.info(
            "Contract parsed: default=%s, allow=%d, confirm=%d, block=%d, "
            "overrides=%d, proactivity=%s, privacy=%s",
            rules.default, len(rules.allow), len(rules.confirm),
            len(rules.block), len(rules.overrides),
            rules.proactivity, rules.privacy_mode,
        )
        return rules

    @staticmethod
    def _extract_yaml(content: str) -> str | None:
        """Extract the first ```yaml ... ``` block from markdown."""
        match = re.search(r"```yaml\s*\n(.*?)```", content, re.DOTALL)
        if match:
            return match.group(1)
        return None

    def _apply_override(self, action: str, integration: str, override: str) -> ActionResult:
        """Apply a per-integration override."""
        if override == "block_all":
            return ActionResult(
                action=action, allowed=False,
                reason=f"Integration '{integration}' is blocked by contract override",
            )
        if override == "allow_send":
            return ActionResult(action=action, allowed=True)
        if override == "confirm_all":
            return ActionResult(
                action=action, allowed=True,
                requires_confirmation=True,
                reason=f"Integration '{integration}' requires confirmation",
            )
        # Unknown override — default deny
        return ActionResult(
            action=action, allowed=False,
            reason=f"Unknown override '{override}' for {integration}",
        )

    def _audit(self, action: str, integration: str | None, result: ActionResult) -> None:
        """Log the contract check to audit trail."""
        if self._audit_fn:
            status = "allowed" if result.allowed else "blocked"
            if result.requires_confirmation:
                status = "needs_confirmation"
            self._audit_fn(
                action=f"contract_check:{action}",
                target=integration or "",
                approved=status,
            )
