"""
Agent Compliance & Audit — Enterprise accountability for AI agent actions.

Provides an immutable audit trail, policy-based access control, violation
detection, and compliance reporting. Every agent action (messages sent, tasks
created, files changed, jobs posted, hires made) can be logged and checked
against a configurable policy engine.

Storage layout:
  state/broker/compliance/
    audit.jsonl                  — immutable append-only audit log
    policies/{policy_id}.json    — defined compliance policies
    violations.jsonl             — detected policy violations

Functions are sync (no async) for use from CLI and MCP tool wrappers.
"""

import json
import uuid
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Optional

from brynq.server import (
    BROKER_DIR,
    SESSION_ID,
    SESSION_NAME,
    PLATFORM,
    _append_jsonl_sync,
    _write_json_sync,
    _read_jsonl_sync,
)

# ── Paths ──────────────────────────────────────────────────────────────────

COMPLIANCE_DIR = BROKER_DIR / "compliance"
POLICIES_DIR = COMPLIANCE_DIR / "policies"
AUDIT_LOG_PATH = COMPLIANCE_DIR / "audit.jsonl"
VIOLATIONS_PATH = COMPLIANCE_DIR / "violations.jsonl"

for _d in (COMPLIANCE_DIR, POLICIES_DIR):
    _d.mkdir(parents=True, exist_ok=True)

# ── Valid Constants ────────────────────────────────────────────────────────

VALID_ACTION_TYPES = (
    "message_sent",
    "task_created",
    "task_accepted",
    "task_completed",
    "task_failed",
    "file_changed",
    "file_locked",
    "file_released",
    "job_posted",
    "job_applied",
    "hire_made",
    "policy_defined",
    "policy_updated",
    "policy_deleted",
    "access_granted",
    "access_revoked",
    "custom",
)

VALID_RULE_TYPES = (
    "no_self_approve",
    "min_reviewers",
    "channel_access",
    "tool_access",
    "rate_limit",
    "priority_restriction",
    "data_retention",
    "custom",
)


# ── Audit Log (Append-Only) ───────────────────────────────────────────────


def log_action(
    action_type: str,
    details: dict,
    channel: Optional[str] = None,
    target: Optional[str] = None,
) -> str:
    """Append an action to the immutable audit log.

    The audit log is strictly append-only. Entries are never modified or
    deleted — this is the core invariant for compliance.

    Args:
        action_type: Category of the action (see VALID_ACTION_TYPES).
        details: Arbitrary dict describing the action specifics.
        channel: Optional broker channel the action relates to.
        target: Optional target entity (file path, task ID, job ID, etc.).

    Returns:
        The generated entry ID.

    Raises:
        ValueError: If action_type is not recognized.
    """
    if action_type not in VALID_ACTION_TYPES:
        raise ValueError(
            f"Invalid action_type '{action_type}'. "
            f"Must be one of: {', '.join(VALID_ACTION_TYPES)}"
        )

    entry_id = uuid.uuid4().hex[:16]
    entry = {
        "entry_id": entry_id,
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "session_id": SESSION_ID,
        "session_name": SESSION_NAME,
        "platform": PLATFORM,
        "action_type": action_type,
        "channel": channel,
        "target": target,
        "details": details,
    }

    _append_jsonl_sync(AUDIT_LOG_PATH, entry)
    return entry_id


# ── Policy Engine ──────────────────────────────────────────────────────────


def define_policy(
    name: str,
    description: str,
    rule_type: str,
    rule_config: dict,
) -> str:
    """Create or update a compliance policy.

    Policies define rules that govern agent behavior. The policy engine
    evaluates these rules during ``check_compliance`` calls.

    Args:
        name: Human-readable policy name (e.g., "No Self-Approve High Tasks").
        description: Longer explanation of the policy purpose.
        rule_type: One of VALID_RULE_TYPES. Determines how rule_config
            is interpreted.
        rule_config: Configuration dict specific to the rule_type:
            - no_self_approve: {"min_priority": "high"}
            - min_reviewers: {"action_type": "task_completed", "min_count": 2,
                              "scope": "code_review"}
            - channel_access: {"allowed_sessions": [...], "channels": [...]}
            - tool_access: {"allowed_sessions": [...], "tools": [...]}
            - rate_limit: {"action_type": "message_sent", "max_per_hour": 100}
            - priority_restriction: {"session_patterns": [...],
                                     "max_priority": "normal"}
            - data_retention: {"max_age_days": 90, "scope": "audit"}
            - custom: {"check_fn_name": "...", "params": {...}}

    Returns:
        The generated policy_id.

    Raises:
        ValueError: If rule_type is not recognized.
    """
    if rule_type not in VALID_RULE_TYPES:
        raise ValueError(
            f"Invalid rule_type '{rule_type}'. "
            f"Must be one of: {', '.join(VALID_RULE_TYPES)}"
        )

    policy_id = uuid.uuid4().hex[:12]
    now = datetime.now(timezone.utc).isoformat()

    policy = {
        "policy_id": policy_id,
        "name": name,
        "description": description,
        "rule_type": rule_type,
        "rule_config": rule_config,
        "active": True,
        "created_by": SESSION_NAME,
        "created_by_session": SESSION_ID,
        "created_at": now,
        "updated_at": now,
    }

    policy_path = POLICIES_DIR / f"{policy_id}.json"
    _write_json_sync(policy_path, policy)

    # Audit the policy creation itself
    log_action(
        "policy_defined",
        {
            "policy_id": policy_id,
            "name": name,
            "rule_type": rule_type,
        },
        target=policy_id,
    )

    return policy_id


def list_policies(active_only: bool = True) -> list[dict]:
    """List all compliance policies.

    Args:
        active_only: If True, return only active policies. If False,
            return all policies including deactivated ones.

    Returns:
        List of policy dicts, sorted by creation time (newest first).
    """
    policies = []
    for f in POLICIES_DIR.glob("*.json"):
        try:
            data = json.loads(f.read_text("utf-8"))
        except (json.JSONDecodeError, OSError):
            continue
        if active_only and not data.get("active", True):
            continue
        policies.append(data)

    policies.sort(key=lambda p: p.get("created_at", ""), reverse=True)
    return policies


def _read_policy(policy_id: str) -> Optional[dict]:
    """Read a single policy by ID."""
    path = POLICIES_DIR / f"{policy_id}.json"
    if not path.exists():
        return None
    try:
        return json.loads(path.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        return None


# ── Compliance Checking ────────────────────────────────────────────────────

# Priority ordering for comparison
_PRIORITY_LEVELS = {"low": 0, "normal": 1, "high": 2, "urgent": 3}


def check_compliance(
    action_type: str,
    details: dict,
) -> tuple[bool, list[dict]]:
    """Check whether an action complies with all active policies.

    Evaluates every active policy against the proposed action. Returns
    whether the action is allowed and a list of any violations found.

    Args:
        action_type: The type of action being proposed.
        details: Action-specific details. Should include relevant context
            such as session_id, channel, priority, etc.

    Returns:
        Tuple of (allowed, violations) where:
          - allowed: True if no violations were found.
          - violations: List of violation dicts, each containing
            policy_id, policy_name, rule_type, and reason.
    """
    violations: list[dict] = []
    policies = list_policies(active_only=True)

    for policy in policies:
        violation = _evaluate_policy(policy, action_type, details)
        if violation is not None:
            violations.append(violation)

    allowed = len(violations) == 0

    # Log violations to the violations file
    if violations:
        for v in violations:
            violation_entry = {
                "violation_id": uuid.uuid4().hex[:12],
                "timestamp": datetime.now(timezone.utc).isoformat(),
                "session_id": details.get("session_id", SESSION_ID),
                "session_name": details.get("session_name", SESSION_NAME),
                "platform": details.get("platform", PLATFORM),
                "action_type": action_type,
                "policy_id": v["policy_id"],
                "policy_name": v["policy_name"],
                "rule_type": v["rule_type"],
                "reason": v["reason"],
                "details": details,
            }
            _append_jsonl_sync(VIOLATIONS_PATH, violation_entry)

    return allowed, violations


def _evaluate_policy(
    policy: dict,
    action_type: str,
    details: dict,
) -> Optional[dict]:
    """Evaluate a single policy against an action.

    Returns a violation dict if the policy is breached, or None if compliant.
    """
    rule_type = policy.get("rule_type", "")
    config = policy.get("rule_config", {})

    if rule_type == "no_self_approve":
        return _check_no_self_approve(policy, action_type, details, config)
    elif rule_type == "min_reviewers":
        return _check_min_reviewers(policy, action_type, details, config)
    elif rule_type == "channel_access":
        return _check_channel_access(policy, action_type, details, config)
    elif rule_type == "tool_access":
        return _check_tool_access(policy, action_type, details, config)
    elif rule_type == "rate_limit":
        return _check_rate_limit(policy, action_type, details, config)
    elif rule_type == "priority_restriction":
        return _check_priority_restriction(policy, action_type, details, config)
    elif rule_type == "data_retention":
        return _check_data_retention(policy, action_type, details, config)
    elif rule_type == "custom":
        return _check_custom(policy, action_type, details, config)

    return None


def _make_violation(policy: dict, reason: str) -> dict:
    """Build a standard violation dict from a policy and reason."""
    return {
        "policy_id": policy["policy_id"],
        "policy_name": policy["name"],
        "rule_type": policy["rule_type"],
        "reason": reason,
    }


def _check_no_self_approve(
    policy: dict, action_type: str, details: dict, config: dict
) -> Optional[dict]:
    """No agent can self-approve tasks over a given priority threshold.

    Config keys:
        min_priority: Minimum priority level that triggers the rule
            (default: "high"). Actions at or above this level cannot be
            self-approved.
    """
    # Only applies to task completion/acceptance actions
    if action_type not in ("task_completed", "task_accepted"):
        return None

    min_priority = config.get("min_priority", "high")
    task_priority = details.get("priority", "normal")
    min_level = _PRIORITY_LEVELS.get(min_priority, 2)
    task_level = _PRIORITY_LEVELS.get(task_priority, 1)

    if task_level < min_level:
        return None

    # Check if the requester and the agent acting are the same session
    requester = details.get("requester_session", "")
    actor = details.get("session_id", SESSION_ID)

    if requester and requester == actor:
        return _make_violation(
            policy,
            f"Self-approval not allowed for {task_priority}-priority tasks. "
            f"Session '{actor}' created and is attempting to approve the same task.",
        )

    return None


def _check_min_reviewers(
    policy: dict, action_type: str, details: dict, config: dict
) -> Optional[dict]:
    """Require a minimum number of distinct reviewers for certain actions.

    Config keys:
        action_type: Which action type this applies to (e.g., "task_completed").
        min_count: Minimum number of distinct sessions required.
        scope: Optional scope filter (e.g., "code_review") matched against
            details["scope"] or details["title"].
    """
    target_action = config.get("action_type", "")
    if action_type != target_action:
        return None

    scope = config.get("scope", "")
    if scope:
        detail_scope = details.get("scope", "")
        detail_title = details.get("title", "")
        if scope not in detail_scope and scope not in detail_title:
            return None

    min_count = config.get("min_count", 2)
    reviewer_count = details.get("reviewer_count", 1)

    if reviewer_count < min_count:
        return _make_violation(
            policy,
            f"Requires at least {min_count} reviewers, but only "
            f"{reviewer_count} provided.",
        )

    return None


def _check_channel_access(
    policy: dict, action_type: str, details: dict, config: dict
) -> Optional[dict]:
    """Restrict which sessions can access specific channels.

    Config keys:
        allowed_sessions: List of session IDs or names that are allowed.
        channels: List of channel names this restriction applies to.
    """
    channel = details.get("channel", "")
    if not channel:
        return None

    restricted_channels = config.get("channels", [])
    if channel not in restricted_channels:
        return None

    allowed = config.get("allowed_sessions", [])
    actor_id = details.get("session_id", SESSION_ID)
    actor_name = details.get("session_name", SESSION_NAME)

    if actor_id in allowed or actor_name in allowed:
        return None

    return _make_violation(
        policy,
        f"Session '{actor_name}' ({actor_id}) is not authorized to access "
        f"channel '{channel}'.",
    )


def _check_tool_access(
    policy: dict, action_type: str, details: dict, config: dict
) -> Optional[dict]:
    """Restrict which sessions can use specific tools.

    Config keys:
        allowed_sessions: List of session IDs or names that are allowed.
        tools: List of tool names this restriction applies to.
    """
    tool = details.get("tool", "")
    if not tool:
        return None

    restricted_tools = config.get("tools", [])
    if tool not in restricted_tools:
        return None

    allowed = config.get("allowed_sessions", [])
    actor_id = details.get("session_id", SESSION_ID)
    actor_name = details.get("session_name", SESSION_NAME)

    if actor_id in allowed or actor_name in allowed:
        return None

    return _make_violation(
        policy,
        f"Session '{actor_name}' ({actor_id}) is not authorized to use "
        f"tool '{tool}'.",
    )


def _check_rate_limit(
    policy: dict, action_type: str, details: dict, config: dict
) -> Optional[dict]:
    """Enforce rate limits on specific action types.

    Config keys:
        action_type: Which action type to rate-limit.
        max_per_hour: Maximum number of actions per hour per session.
    """
    target_action = config.get("action_type", "")
    if action_type != target_action:
        return None

    max_per_hour = config.get("max_per_hour", 100)
    actor_id = details.get("session_id", SESSION_ID)

    # Count recent actions from this session in the audit log
    cutoff = datetime.now(timezone.utc) - timedelta(hours=1)
    recent_count = _count_recent_actions(action_type, actor_id, cutoff)

    if recent_count >= max_per_hour:
        return _make_violation(
            policy,
            f"Rate limit exceeded: {recent_count}/{max_per_hour} "
            f"'{action_type}' actions in the last hour for session '{actor_id}'.",
        )

    return None


def _check_priority_restriction(
    policy: dict, action_type: str, details: dict, config: dict
) -> Optional[dict]:
    """Restrict certain sessions from creating actions above a priority.

    Config keys:
        session_patterns: List of session name substrings to match.
        max_priority: Maximum allowed priority for matched sessions.
    """
    task_priority = details.get("priority", "")
    if not task_priority:
        return None

    session_patterns = config.get("session_patterns", [])
    actor_name = details.get("session_name", SESSION_NAME)

    # Check if the actor matches any restricted pattern
    matched = any(pattern in actor_name for pattern in session_patterns)
    if not matched:
        return None

    max_priority = config.get("max_priority", "normal")
    max_level = _PRIORITY_LEVELS.get(max_priority, 1)
    task_level = _PRIORITY_LEVELS.get(task_priority, 1)

    if task_level > max_level:
        return _make_violation(
            policy,
            f"Session '{actor_name}' is restricted to max priority "
            f"'{max_priority}', but attempted '{task_priority}'.",
        )

    return None


def _check_data_retention(
    policy: dict, action_type: str, details: dict, config: dict
) -> Optional[dict]:
    """Enforce data retention lifecycle policies.

    Config keys:
        max_age_days: Maximum age in days for data in the specified scope.
        scope: Which data scope this applies to ("audit", "violations",
            "channels", etc.).

    This check does not block actions — it flags a violation if data
    older than max_age_days exists in the specified scope, serving as a
    reminder that cleanup is overdue.
    """
    # Only evaluate on explicit retention-check actions
    if action_type != "custom":
        return None

    check_scope = details.get("scope", "")
    policy_scope = config.get("scope", "")
    if check_scope != policy_scope:
        return None

    max_age_days = config.get("max_age_days", 90)
    cutoff = datetime.now(timezone.utc) - timedelta(days=max_age_days)

    oldest_ts = details.get("oldest_entry_timestamp", "")
    if not oldest_ts:
        return None

    try:
        oldest = datetime.fromisoformat(oldest_ts)
        if oldest.tzinfo is None:
            oldest = oldest.replace(tzinfo=timezone.utc)
    except (ValueError, TypeError):
        return None

    if oldest < cutoff:
        age_days = (datetime.now(timezone.utc) - oldest).days
        return _make_violation(
            policy,
            f"Data retention policy breached: oldest entry in '{policy_scope}' "
            f"is {age_days} days old (max allowed: {max_age_days} days).",
        )

    return None


def _check_custom(
    policy: dict, action_type: str, details: dict, config: dict
) -> Optional[dict]:
    """Evaluate a custom policy rule.

    Custom rules match on a named check and arbitrary parameters. The
    caller is responsible for passing the appropriate details. This
    provides an extension point without requiring code changes.

    Config keys:
        match_action: Action type(s) this custom rule applies to
            (string or list of strings).
        required_fields: List of field names that must be present in details.
        forbidden_values: Dict of {field: [forbidden_values]} to reject.
    """
    match_action = config.get("match_action", "")
    if match_action:
        if isinstance(match_action, str):
            match_action = [match_action]
        if action_type not in match_action:
            return None

    # Check required fields
    required_fields = config.get("required_fields", [])
    for field in required_fields:
        if field not in details or details[field] is None:
            return _make_violation(
                policy,
                f"Required field '{field}' is missing from action details.",
            )

    # Check forbidden values
    forbidden_values = config.get("forbidden_values", {})
    for field, forbidden in forbidden_values.items():
        value = details.get(field, "")
        if value in forbidden:
            return _make_violation(
                policy,
                f"Field '{field}' has forbidden value '{value}'.",
            )

    return None


# ── Audit Log Queries ──────────────────────────────────────────────────────


def get_audit_log(
    hours: int = 24,
    action_type: Optional[str] = None,
    agent: Optional[str] = None,
) -> list[dict]:
    """Query the audit log with optional time, type, and agent filters.

    Args:
        hours: How far back to look (default 24 hours).
        action_type: Filter by action type. None returns all types.
        agent: Filter by session ID or session name substring.
            None returns all agents.

    Returns:
        List of audit entries matching the filters, newest first.
    """
    entries = _read_jsonl_sync(AUDIT_LOG_PATH)
    cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)

    filtered = []
    for entry in entries:
        # Time filter
        ts_str = entry.get("timestamp", "")
        try:
            ts = datetime.fromisoformat(ts_str)
            if ts.tzinfo is None:
                ts = ts.replace(tzinfo=timezone.utc)
        except (ValueError, TypeError):
            continue
        if ts < cutoff:
            continue

        # Action type filter
        if action_type is not None and entry.get("action_type") != action_type:
            continue

        # Agent filter (match against session_id or session_name)
        if agent is not None:
            entry_sid = entry.get("session_id", "")
            entry_sname = entry.get("session_name", "")
            if agent not in entry_sid and agent not in entry_sname:
                continue

        filtered.append(entry)

    # Newest first
    filtered.sort(key=lambda e: e.get("timestamp", ""), reverse=True)
    return filtered


# ── Compliance Reports ─────────────────────────────────────────────────────


def get_compliance_report(hours: int = 24) -> dict:
    """Generate a compliance summary report over a time window.

    Args:
        hours: How far back to analyze (default 24 hours).

    Returns:
        Dict with:
          - period_hours: The reporting period.
          - total_actions: Number of audit entries in the window.
          - actions_by_type: Breakdown of action counts by type.
          - total_violations: Number of violations in the window.
          - violations_by_policy: Breakdown of violations by policy name.
          - violations_by_agent: Breakdown of violations by session name.
          - compliance_rate: Percentage of actions without violations
              (0.0 to 100.0).
          - active_policies: Number of currently active policies.
          - top_agents: Top agents by action count.
          - generated_at: Timestamp of report generation.
    """
    cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)

    # Audit log analysis
    all_entries = _read_jsonl_sync(AUDIT_LOG_PATH)
    actions_in_window = []
    for entry in all_entries:
        ts_str = entry.get("timestamp", "")
        try:
            ts = datetime.fromisoformat(ts_str)
            if ts.tzinfo is None:
                ts = ts.replace(tzinfo=timezone.utc)
        except (ValueError, TypeError):
            continue
        if ts >= cutoff:
            actions_in_window.append(entry)

    total_actions = len(actions_in_window)

    # Actions by type
    actions_by_type: dict[str, int] = {}
    agent_counts: dict[str, int] = {}
    for entry in actions_in_window:
        atype = entry.get("action_type", "unknown")
        actions_by_type[atype] = actions_by_type.get(atype, 0) + 1

        agent_name = entry.get("session_name", "unknown")
        agent_counts[agent_name] = agent_counts.get(agent_name, 0) + 1

    # Violations analysis
    all_violations = _read_jsonl_sync(VIOLATIONS_PATH)
    violations_in_window = []
    for v in all_violations:
        ts_str = v.get("timestamp", "")
        try:
            ts = datetime.fromisoformat(ts_str)
            if ts.tzinfo is None:
                ts = ts.replace(tzinfo=timezone.utc)
        except (ValueError, TypeError):
            continue
        if ts >= cutoff:
            violations_in_window.append(v)

    total_violations = len(violations_in_window)

    # Violations by policy
    violations_by_policy: dict[str, int] = {}
    violations_by_agent: dict[str, int] = {}
    for v in violations_in_window:
        pname = v.get("policy_name", "unknown")
        violations_by_policy[pname] = violations_by_policy.get(pname, 0) + 1

        agent_name = v.get("session_name", "unknown")
        violations_by_agent[agent_name] = violations_by_agent.get(agent_name, 0) + 1

    # Compliance rate
    if total_actions > 0:
        compliant_actions = max(0, total_actions - total_violations)
        compliance_rate = round((compliant_actions / total_actions) * 100.0, 2)
    else:
        compliance_rate = 100.0

    # Top agents by action count (sorted descending)
    top_agents = sorted(
        [{"agent": name, "actions": count} for name, count in agent_counts.items()],
        key=lambda x: x["actions"],
        reverse=True,
    )[:10]

    return {
        "period_hours": hours,
        "total_actions": total_actions,
        "actions_by_type": actions_by_type,
        "total_violations": total_violations,
        "violations_by_policy": violations_by_policy,
        "violations_by_agent": violations_by_agent,
        "compliance_rate": compliance_rate,
        "active_policies": len(list_policies(active_only=True)),
        "top_agents": top_agents,
        "generated_at": datetime.now(timezone.utc).isoformat(),
    }


# ── Internal Helpers ───────────────────────────────────────────────────────


def _count_recent_actions(
    action_type: str,
    session_id: str,
    cutoff: datetime,
) -> int:
    """Count audit log entries matching an action type and session since cutoff."""
    entries = _read_jsonl_sync(AUDIT_LOG_PATH)
    count = 0
    for entry in entries:
        if entry.get("action_type") != action_type:
            continue
        if entry.get("session_id") != session_id:
            continue
        ts_str = entry.get("timestamp", "")
        try:
            ts = datetime.fromisoformat(ts_str)
            if ts.tzinfo is None:
                ts = ts.replace(tzinfo=timezone.utc)
        except (ValueError, TypeError):
            continue
        if ts >= cutoff:
            count += 1
    return count
