"""Local policy evaluation for active agent commands (0.1.19).

Workspace routing modes (owner spec, 0.2.2.35 / production 0.2.2.45):
- Safe: writes only ~/IuVeWorkspace unless trusted + file_access grant (still Safe → workspace).
- Balanced: writes to Desktop/Downloads/Documents when file_access + trusted.
- Trusted: full home write scopes when file_access + trusted.
Publish/send/submit and root always require explicit user confirmation (stop-zone).
Screen tasks require screen_vision grant from Permission Center.

P2 low-friction (0.2.2.52): trusted + plan-bound + expert/full preset skips per-step
modals for in-scope commands — see TRUSTED_AUTONOMOUS_PROFILE in trusted_autonomous_policy.
"""

from __future__ import annotations

import os
from dataclasses import dataclass, field
from typing import Any, Protocol

from iuve_connect_agents.shared.confirm_type import ConfirmType
from iuve_connect_agents.shared.action_session_policy import command_always_requires_confirm
from iuve_connect_agents.shared.production_policy import (
    is_publish_submit_action,
    is_vision_command,
    resolve_publish_stop_zone_tag,
    root_operations_denial_message,
    routing_write_denial_message,
    screen_vision_denial_message,
)
from iuve_connect_agents.shared.stop_zone import StopZone

from .capability_resolver import (
    resolve_workspace_path,
    resolve_workspace_routing_mode,
    validate_write_path_for_policy,
)
from .command_models import (
    BACKEND_CAPABILITY_TO_CANONICAL,
    CommandEnvelope,
    PolicyDecision,
)
from .policy_judge import PolicyJudge

ALLOWED_DEVICE_COMMAND_TYPES = frozenset(
    {
        "get_status",
        "get_diagnostics",
        "collect_logs",
        "restart_agent",
        "check_update",
        "apply_update",
        "open_local_console_notification_only",
        "read_file_in_workspace",
        "write_file_in_workspace",
        "browser_open_url",
        "open_url",
        "open_document_edit",
        "service_restart",
        "package_install_dry_run",
        "network_diagnostics",
        "web_fetch_bounded",
        "summarize_content",
        "generate_content",
        "insert_exact_text",
        "verify_workspace_file",
        "social_post_draft",
        "email_compose",
        "social_post_publish",
        "browser_click_publish",
        "email_send",
        "mouse_move_relative",
        "empty_trash",
        "open_folder",
        "application_launch",
        "keyboard_type_text",
        "capture_screenshot",
        "verify_screen_contains_text",
        "verify_web_draft",
        "verify_published",
        "open_web_composer",
        "describe_active_window",
        "list_files_in_folder",
        "plan_step",
        "message_send",
        "comment_submit",
        "payment_execute",
        "subscription_confirm",
        "folder_wipe",
        "db_drop",
        "package_remove",
        "token_export",
        "2fa_change",
        "firewall_change",
        "install_unknown_software",
        "mass_mail_send",
        "public_accusation_post",
    }
)

DENIED_BY_POLICY_COMMAND_TYPES = frozenset(
    {
        "shell_exec",
        "file_delete",
        "system_change",
        "credential_access",
    }
)

COMMAND_REQUIRED_CAPABILITIES: dict[str, str] = {
    "get_status": "agent.status.read",
    "get_diagnostics": "agent.diagnostics.read",
    "collect_logs": "agent.logs.collect",
    "restart_agent": "agent.lifecycle.restart",
    "check_update": "agent.update.check",
    "apply_update": "agent.update.apply",
    "open_local_console_notification_only": "agent.ui.notify",
    "read_file_in_workspace": "agent.workspace.file.read",
    "write_file_in_workspace": "agent.workspace.file.write",
    "browser_open_url": "agent.browser.open_url",
    "open_url": "agent.browser.open_url",
    "open_document_edit": "agent.desktop.app",
    "service_restart": "agent.service.restart",
    "package_install_dry_run": "agent.package.install.dry_run",
    "network_diagnostics": "agent.network.diagnostics",
    "web_fetch_bounded": "agent.network.fetch",
    "summarize_content": "agent.content.summarize",
    "generate_content": "agent.content.summarize",
    "insert_exact_text": "agent.content.summarize",
    "verify_workspace_file": "agent.workspace.file.read",
    "social_post_draft": "agent.desktop.app",
    "email_compose": "agent.desktop.app",
    "social_post_publish": "agent.browser.open_url",
    "browser_click_publish": "agent.browser.open_url",
    "email_send": "agent.desktop.app",
    "mouse_move_relative": "agent.desktop.input",
    "empty_trash": "agent.workspace.file.write",
    "open_folder": "agent.workspace.file.read",
    "application_launch": "agent.desktop.app",
    "keyboard_type_text": "agent.desktop.input",
    "capture_screenshot": "agent.screen.capture",
    "verify_screen_contains_text": "agent.screen.verify",
    "verify_web_draft": "agent.screen.verify",
    "verify_published": "agent.screen.verify",
    "open_web_composer": "agent.screen.verify",
    "describe_active_window": "agent.screen.verify",
    "list_files_in_folder": "agent.workspace.file.read",
    "plan_step": "agent.status.read",
    "message_send": "agent.desktop.app",
    "comment_submit": "agent.browser.open_url",
    "payment_execute": "agent.desktop.app",
    "subscription_confirm": "agent.desktop.app",
    "folder_wipe": "agent.workspace.file.write",
    "db_drop": "agent.workspace.file.write",
    "package_remove": "agent.package.install.dry_run",
    "token_export": "agent.workspace.file.read",
    "2fa_change": "agent.desktop.app",
    "firewall_change": "agent.service.restart",
    "install_unknown_software": "agent.package.install.dry_run",
    "mass_mail_send": "agent.desktop.app",
    "public_accusation_post": "agent.browser.open_url",
}

COMMAND_RISK_LEVELS: dict[str, str] = {
    "get_status": "low",
    "get_diagnostics": "low",
    "collect_logs": "low",
    "check_update": "low",
    "open_local_console_notification_only": "low",
    "browser_open_url": "medium",
    "open_url": "medium",
    "restart_agent": "medium",
    "read_file_in_workspace": "medium",
    "write_file_in_workspace": "medium",
    "open_document_edit": "medium",
    "network_diagnostics": "medium",
    "web_fetch_bounded": "medium",
    "summarize_content": "medium",
    "generate_content": "medium",
    "insert_exact_text": "low",
    "verify_workspace_file": "low",
    "apply_update": "high",
    "service_restart": "high",
    "package_install_dry_run": "high",
    "social_post_draft": "medium",
    "email_compose": "medium",
    "social_post_publish": "high",
    "browser_click_publish": "high",
    "email_send": "high",
    "mouse_move_relative": "high",
    "empty_trash": "medium",
    "open_folder": "low",
    "application_launch": "medium",
    "keyboard_type_text": "high",
    "capture_screenshot": "medium",
    "verify_screen_contains_text": "low",
    "verify_web_draft": "low",
    "verify_published": "low",
    "open_web_composer": "medium",
    "describe_active_window": "low",
    "list_files_in_folder": "low",
    "plan_step": "low",
    "message_send": "high",
    "comment_submit": "high",
    "payment_execute": "critical",
    "subscription_confirm": "critical",
    "folder_wipe": "high",
    "db_drop": "critical",
    "package_remove": "high",
    "token_export": "critical",
    "2fa_change": "critical",
    "firewall_change": "high",
    "install_unknown_software": "high",
    "mass_mail_send": "high",
    "public_accusation_post": "high",
}

SCOPE_REQUIRED_COMMAND_TYPES = frozenset(
    {
        "read_file_in_workspace",
        "write_file_in_workspace",
        "open_document_edit",
        "service_restart",
        "package_install_dry_run",
        "network_diagnostics",
    }
)

IMPLICIT_GRANT_CAPABILITIES = frozenset({"READ_ONLY_SYSTEM_INFO"})


class ActionSessionChecker(Protocol):
    def find_covering_session(
        self,
        *,
        command_type: str,
        canonical_capability: str,
        envelope_scope: dict[str, Any],
        payload: dict[str, Any],
        plan_id: str = "",
    ) -> Any | None: ...

    def should_skip_confirmation(
        self,
        *,
        session: Any,
        risk_level: str,
        command_type: str,
        canonical_capability: str,
        envelope_scope: dict[str, Any],
        payload: dict[str, Any],
        trust_state: str = "",
    ) -> bool: ...


class CapabilityGrantChecker(Protocol):
    def is_granted(self, capability_key: str, *, scope: dict[str, Any] | None = None) -> bool: ...

    def is_expired(self, capability_key: str) -> bool: ...


@dataclass
class PolicyContext:
    trust_state: str = "connected"
    capability_store: CapabilityGrantChecker | None = None
    action_session_store: ActionSessionChecker | None = None
    always_allow_types: frozenset[str] = field(default_factory=frozenset)
    implicit_read_only: bool = True
    routing_mode: str = ""


class PolicyEngine:
    def __init__(self, *, policy_judge: PolicyJudge | None = None) -> None:
        self._policy_judge = policy_judge or PolicyJudge()

    def evaluate(self, envelope: CommandEnvelope, context: PolicyContext) -> PolicyDecision:
        command_type = envelope.command_type
        payload = envelope.payload

        if context.trust_state.strip().lower() != "trusted":
            return self._deny(
                "trust_not_trusted",
                "permission_denied:trust_not_trusted",
            )

        if command_type in DENIED_BY_POLICY_COMMAND_TYPES:
            return self._deny(
                "command_rejected_by_policy",
                "permission_denied:command_rejected_by_policy",
            )

        if command_type not in ALLOWED_DEVICE_COMMAND_TYPES:
            return self._deny(
                "unsupported_command_type",
                "unsupported_command_type",
            )

        expected_capability = COMMAND_REQUIRED_CAPABILITIES.get(command_type, "")
        payload_capability = envelope.required_capability
        if not expected_capability or payload_capability != expected_capability:
            return self._deny(
                "capability_mismatch",
                "permission_denied:capability_mismatch",
            )

        canonical_key = BACKEND_CAPABILITY_TO_CANONICAL.get(expected_capability, "")
        if not canonical_key:
            return self._deny(
                "capability_mismatch",
                "permission_denied:capability_mismatch",
            )

        consent = str(payload.get("user_consent_state") or "").strip()
        if not consent:
            return self._deny(
                "missing_user_consent_state",
                "permission_denied:missing_user_consent_state",
            )

        risk_level = self._resolve_risk_level(envelope, command_type)

        elevation_error = self._check_elevation(payload, risk_level, command_type)
        if elevation_error:
            return elevation_error

        rollback_error = self._check_rollback(payload, risk_level, command_type)
        if rollback_error:
            return rollback_error

        ai_metadata_error = self._check_ai_metadata(payload, risk_level)
        if ai_metadata_error:
            return ai_metadata_error

        if is_vision_command(command_type):
            vision_error = self._check_screen_vision_grant(context, canonical_key=canonical_key)
            if vision_error:
                return vision_error

        scope_error = self._check_scope(envelope, command_type)
        if scope_error:
            return scope_error

        routing_mode = self._resolve_routing_mode(context, payload)
        routing_error = self._check_routing_write_policy(
            envelope,
            command_type,
            context,
            routing_mode=routing_mode,
        )
        if routing_error:
            return routing_error

        root_error = self._check_root_operations_grant(context, payload)
        if root_error:
            return root_error

        grant_error = self._check_capability_grant(
            context,
            canonical_key=canonical_key,
            scope=envelope.scope,
        )
        if grant_error:
            return grant_error

        step_text = str(
            payload.get("step_label")
            or payload.get("goal_summary")
            or payload.get("reasoning_summary")
            or ""
        )
        publish_zone = resolve_publish_stop_zone_tag(command_type, payload, step_text=step_text)
        stop_zone_tag = payload.get("stop_zone")
        if publish_zone is not None and not stop_zone_tag:
            stop_zone_tag = publish_zone.value

        judge = self._policy_judge.evaluate_step(
            command_type,
            stop_zone_tag=stop_zone_tag,
            risk_level=risk_level,
        )
        confirm_type = judge.confirm_type
        stop_zone_value = judge.stop_zone.value if judge.stop_zone else None

        require_confirmation = judge.require_confirmation or risk_level in {"medium", "high", "critical"}
        if is_publish_submit_action(command_type, payload) or command_always_requires_confirm(command_type, payload):
            require_confirmation = True
            if publish_zone is not None or stop_zone_value == StopZone.PUBLISH.value:
                stop_zone_value = StopZone.PUBLISH.value
                confirm_type = ConfirmType.DOUBLE_CONFIRM
        if command_type in context.always_allow_types and not is_publish_submit_action(command_type, payload):
            if str(payload.get("execution_profile") or "user").strip().lower() != "root":
                require_confirmation = False
                confirm_type = ConfirmType.NONE

        plan_id = str(payload.get("plan_id") or "").strip()
        action_session_id = ""
        confirmation_skip_reason = ""

        session_may_skip = (
            confirm_type not in {ConfirmType.DOUBLE_CONFIRM}
            and stop_zone_value is None
            and not command_always_requires_confirm(command_type, payload)
            and not is_publish_submit_action(command_type, payload)
        )

        session_store = context.action_session_store
        if session_store is not None and require_confirmation and session_may_skip:
            covering = session_store.find_covering_session(
                command_type=command_type,
                canonical_capability=canonical_key,
                envelope_scope=envelope.scope,
                payload=payload,
                plan_id=plan_id,
            )
            if covering is not None and session_store.should_skip_confirmation(
                session=covering,
                risk_level=risk_level,
                command_type=command_type,
                canonical_capability=canonical_key,
                envelope_scope=envelope.scope,
                payload=payload,
                trust_state=context.trust_state,
            ):
                require_confirmation = False
                action_session_id = str(getattr(covering, "session_id", "") or "")
                confirmation_skip_reason = "covered_by_action_session"
                if confirm_type == ConfirmType.MODAL:
                    confirm_type = ConfirmType.SESSION

        if not require_confirmation and confirm_type == ConfirmType.MODAL:
            confirm_type = ConfirmType.NONE

        return PolicyDecision(
            allowed=True,
            capability_key=canonical_key,
            require_confirmation=require_confirmation,
            confirm_type=confirm_type.value,
            stop_zone=stop_zone_value,
            action_session_id=action_session_id,
            confirmation_skip_reason=confirmation_skip_reason,
            plan_id=plan_id,
        )

    def _resolve_risk_level(self, envelope: CommandEnvelope, command_type: str) -> str:
        risk = envelope.risk_level
        if risk in {"low", "medium", "high", "critical"}:
            return risk
        return COMMAND_RISK_LEVELS.get(command_type, "low")

    def _check_elevation(
        self,
        payload: dict[str, Any],
        risk_level: str,
        command_type: str,
    ) -> PolicyDecision | None:
        execution_profile = str(payload.get("execution_profile") or "user").strip().lower()
        if execution_profile != "root":
            return None

        elevation_state = str(payload.get("elevation_state") or "").strip().lower()
        if elevation_state == "denied":
            return self._deny(
                "elevation_denied",
                "permission_denied:elevation_denied",
            )

        if elevation_state != "approved":
            return None

        approval_ticket = str(payload.get("approval_ticket") or "").strip()
        if len(approval_ticket) < 6:
            return self._deny(
                "approval_ticket_missing",
                "permission_denied:approval_ticket_missing",
            )

        rollback_instruction = str(payload.get("rollback_instruction") or "").strip()
        if len(rollback_instruction) < 12:
            return self._deny(
                "rollback_instruction_missing",
                "permission_denied:rollback_instruction_missing",
            )

        if risk_level in {"high", "critical"} and command_type in {"restart_agent", "collect_logs"}:
            capability_subject = str(payload.get("capability_subject") or "").strip().lower()
            if not capability_subject:
                return self._deny(
                    "missing_capability_subject",
                    "permission_denied:missing_capability_subject",
                )

        return None

    def _resolve_routing_mode(self, context: PolicyContext, payload: dict[str, Any]) -> str:
        explicit = str(context.routing_mode or payload.get("routing_mode") or "").strip().lower()
        if explicit in {"safe", "balanced", "trusted"}:
            return explicit
        return resolve_workspace_routing_mode(trust_state=context.trust_state)

    def _file_access_granted(self, context: PolicyContext) -> bool:
        store = context.capability_store
        if store is None:
            return False
        is_granted = getattr(store, "is_granted", None)
        if callable(is_granted):
            try:
                return bool(is_granted("WRITE_FILES_ALLOWED_PATHS"))
            except Exception:
                return False
        manager = getattr(store, "manager", None)
        if manager is not None and hasattr(manager, "is_enabled"):
            try:
                return bool(manager.is_enabled("file_access"))
            except Exception:
                return False
        return False

    def _check_routing_write_policy(
        self,
        envelope: CommandEnvelope,
        command_type: str,
        context: PolicyContext,
        *,
        routing_mode: str,
    ) -> PolicyDecision | None:
        if command_type not in {"write_file_in_workspace", "open_document_edit"}:
            return None

        workspace_paths = envelope.scope.get("workspace_paths")
        if not isinstance(workspace_paths, list) or not workspace_paths:
            workspace_paths = [str(resolve_workspace_path())]

        target = self._resolve_workspace_target(envelope.payload, workspace_paths)
        if target is None:
            return self._deny(
                "path_outside_scope",
                routing_write_denial_message(hint="путь вне области записи"),
            )

        allowed, hint = validate_write_path_for_policy(
            target,
            routing_mode=routing_mode,  # type: ignore[arg-type]
            trust_state=context.trust_state,
            file_access_granted=self._file_access_granted(context),
            scope_workspace_paths=workspace_paths,
        )
        if allowed:
            return None
        return self._deny("path_outside_routing_scope", routing_write_denial_message(hint=hint))

    def _check_root_operations_grant(
        self,
        context: PolicyContext,
        payload: dict[str, Any],
    ) -> PolicyDecision | None:
        execution_profile = str(payload.get("execution_profile") or "user").strip().lower()
        if execution_profile != "root":
            return None
        manager = getattr(context.capability_store, "manager", None) if context.capability_store else None
        if manager is not None and hasattr(manager, "is_enabled"):
            if manager.is_enabled("root_operations"):
                return None
        return self._deny(
            "root_operations_not_granted",
            root_operations_denial_message(),
        )

    def _check_screen_vision_grant(
        self,
        context: PolicyContext,
        *,
        canonical_key: str,
    ) -> PolicyDecision | None:
        if canonical_key == "SCREEN_CAPTURE":
            store = context.capability_store
            if store is not None and store.is_granted(canonical_key):
                return None
            return self._deny(
                "screen_vision_grant_required",
                screen_vision_denial_message(),
            )
        return None

    def _check_rollback(
        self,
        payload: dict[str, Any],
        risk_level: str,
        command_type: str,
    ) -> PolicyDecision | None:
        if risk_level not in {"high", "critical"}:
            return None

        rollback_hint = str(payload.get("rollback_hint") or "").strip()
        rollback_instruction = str(payload.get("rollback_instruction") or "").strip()
        if not rollback_hint or len(rollback_instruction) < 12:
            return self._deny(
                "rollback_instruction_missing",
                "permission_denied:rollback_instruction_missing",
            )

        if risk_level in {"high", "critical"} and command_type in {"restart_agent", "collect_logs"}:
            capability_subject = str(payload.get("capability_subject") or "").strip().lower()
            if not capability_subject:
                return self._deny(
                    "missing_capability_subject",
                    "permission_denied:missing_capability_subject",
                )

        return None

    def _check_ai_metadata(
        self,
        payload: dict[str, Any],
        risk_level: str,
    ) -> PolicyDecision | None:
        if risk_level not in {"medium", "high", "critical"}:
            return None

        source_agent = str(payload.get("source_agent") or "").strip()
        model_source = str(payload.get("model_source") or "").strip()
        reasoning_summary = str(payload.get("reasoning_summary") or "").strip()
        if not source_agent or not model_source or not reasoning_summary:
            return self._deny(
                "missing_ai_metadata",
                "permission_denied:missing_ai_metadata",
            )

        return None

    def _check_scope(
        self,
        envelope: CommandEnvelope,
        command_type: str,
    ) -> PolicyDecision | None:
        if command_type not in SCOPE_REQUIRED_COMMAND_TYPES:
            return None

        scope = envelope.scope
        if command_type in {"read_file_in_workspace", "write_file_in_workspace"}:
            workspace_paths = scope.get("workspace_paths")
            if not isinstance(workspace_paths, list) or not workspace_paths:
                return self._deny(
                    "missing_scope",
                    "permission_denied:missing_scope",
                )

            target_path = self._resolve_workspace_target(envelope.payload, workspace_paths)
            if target_path is None:
                return self._deny(
                    "path_outside_scope",
                    "permission_denied:path_outside_scope",
                )
            return None

        if command_type == "service_restart":
            allowed_services = scope.get("allowed_services")
            service_name = str(envelope.payload.get("service_name") or "").strip()
            if not isinstance(allowed_services, list) or not allowed_services:
                return self._deny("missing_scope", "permission_denied:missing_scope")
            if service_name not in allowed_services:
                return self._deny(
                    "scope_violation",
                    "permission_denied:path_outside_scope",
                )
            return None

        if command_type == "package_install_dry_run":
            allowed_packages = scope.get("allowed_packages")
            package_name = str(envelope.payload.get("package_name") or "").strip()
            if not isinstance(allowed_packages, list) or not allowed_packages:
                return self._deny("missing_scope", "permission_denied:missing_scope")
            if package_name not in allowed_packages:
                return self._deny(
                    "scope_violation",
                    "permission_denied:path_outside_scope",
                )
            return None

        if command_type == "network_diagnostics":
            allowed_checks = scope.get("allowed_network_checks")
            if not isinstance(allowed_checks, list) or not allowed_checks:
                return self._deny("missing_scope", "permission_denied:missing_scope")
            check_target = str(envelope.payload.get("target") or envelope.payload.get("host") or "").strip()
            if check_target and check_target not in allowed_checks:
                return self._deny(
                    "scope_violation",
                    "permission_denied:path_outside_scope",
                )

        if command_type == "open_document_edit":
            workspace_paths = scope.get("workspace_paths")
            if not isinstance(workspace_paths, list) or not workspace_paths:
                return self._deny("missing_scope", "permission_denied:missing_scope")
            target_path = self._resolve_workspace_target(envelope.payload, workspace_paths)
            if target_path is None:
                return self._deny(
                    "path_outside_scope",
                    "permission_denied:path_outside_scope",
                )

        return None

    @staticmethod
    def _url_allowed(url: str, domain_allowlist: list[Any]) -> bool:
        from urllib.parse import urlparse

        parsed = urlparse(url)
        if parsed.scheme not in {"http", "https"}:
            return False
        host = (parsed.hostname or "").lower()
        if not host:
            return False
        for entry in domain_allowlist:
            pattern = str(entry or "").strip().lower()
            if not pattern:
                continue
            if pattern == "*":
                return True
            if pattern.startswith("*."):
                suffix = pattern[2:]
                if host == suffix or host.endswith(f".{suffix}"):
                    return True
            elif host == pattern:
                return True
        return False

    def _resolve_workspace_target(
        self,
        payload: dict[str, Any],
        workspace_paths: list[Any],
    ) -> str | None:
        relative_path = str(payload.get("relative_path") or "").strip()
        absolute_path = str(payload.get("absolute_path") or payload.get("path") or "").strip()

        roots = [os.path.realpath(str(root)) for root in workspace_paths if str(root).strip()]
        if not roots:
            return None

        if absolute_path:
            candidate = os.path.realpath(absolute_path)
        elif relative_path:
            candidate = os.path.realpath(os.path.join(roots[0], relative_path))
        else:
            return None

        for root in roots:
            root_prefix = root if root.endswith(os.sep) else root + os.sep
            if candidate == root or candidate.startswith(root_prefix):
                return candidate
        return None

    def _check_capability_grant(
        self,
        context: PolicyContext,
        *,
        canonical_key: str,
        scope: dict[str, Any],
    ) -> PolicyDecision | None:
        store = context.capability_store
        if store is None:
            if canonical_key in IMPLICIT_GRANT_CAPABILITIES and context.implicit_read_only:
                return None
            return self._deny(
                "capability_not_granted",
                "permission_denied:capability_not_granted",
            )

        if store.is_expired(canonical_key):
            return self._deny(
                "expired_capability",
                "permission_denied:expired_capability",
            )

        if store.is_granted(canonical_key, scope=scope):
            return None

        if (
            canonical_key in IMPLICIT_GRANT_CAPABILITIES
            and context.implicit_read_only
            and not store.is_expired(canonical_key)
        ):
            return None

        return self._deny(
            "capability_not_granted",
            "permission_denied:capability_not_granted",
        )

    @staticmethod
    def _deny(reason_code: str, error_message: str) -> PolicyDecision:
        return PolicyDecision(
            allowed=False,
            reason_code=reason_code,
            error_message=error_message,
        )
