"""
DuckClaw Permission Engine — THE core differentiator.
4-Tier action approval system:
  SAFE   → auto-approved, no notification
  NOTIFY → auto-approved, user informed
  ASK    → must get explicit user approval before executing
  BLOCK  → never allowed, always rejected

Every action is logged to the audit trail.
"""

import json
import logging
import sqlite3
import asyncio
from enum import Enum
from datetime import datetime
from pathlib import Path
from typing import Optional, Callable, Any
from dataclasses import dataclass

from duckclaw.core.config import PermissionsConfig, MemoryConfig

logger = logging.getLogger(__name__)


AUDIT_SCHEMA = """
CREATE TABLE IF NOT EXISTS audit_log (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    timestamp       TEXT NOT NULL DEFAULT (datetime('now')),
    action_type     TEXT NOT NULL,
    tier            TEXT NOT NULL,
    description     TEXT NOT NULL,
    details         TEXT,
    status          TEXT NOT NULL,
    source          TEXT DEFAULT 'system',
    session_id      TEXT,
    reversible      INTEGER DEFAULT 1,
    risk_level      TEXT DEFAULT 'low'
);

CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_audit_type ON audit_log(action_type);
CREATE INDEX IF NOT EXISTS idx_audit_status ON audit_log(status);
"""

RULES_SCHEMA = """
CREATE TABLE IF NOT EXISTS permission_rules (
    action_type TEXT PRIMARY KEY,
    tier        TEXT NOT NULL
);
"""


class Tier(str, Enum):
    SAFE = "safe"        # No approval needed — answer questions, read memory
    NOTIFY = "notify"    # User informed after — file reads, web search, browse
    ASK = "ask"          # Must approve before — send email, screenshot, shell exec
    BLOCK = "block"      # Never allowed — delete system files, access credentials


# ── Default action classification ─────────────────────────────────────────────
# Conservative defaults: unknown actions default to ASK
DEFAULT_RULES: dict[str, Tier] = {
    # SAFE — pure information, no side effects
    "chat_response":        Tier.SAFE,
    "memory_read":          Tier.SAFE,
    "memory_search":        Tier.SAFE,
    "web_search":           Tier.SAFE,
    "list_files":           Tier.SAFE,
    "list_cameras":         Tier.SAFE,

    # NOTIFY — read-only access, informs user
    "file_read":            Tier.NOTIFY,
    "web_browse":           Tier.NOTIFY,
    "shell_safe":           Tier.NOTIFY,
    "browser_screenshot":   Tier.NOTIFY,
    "extract_text":         Tier.NOTIFY,

    # ASK — has real-world side effects
    "screen_capture":       Tier.ASK,
    "camera_capture":       Tier.ASK,
    "file_write":           Tier.ASK,
    "file_delete":          Tier.ASK,
    "send_email":           Tier.ASK,
    "send_message":         Tier.ASK,
    "shell_exec":           Tier.ASK,
    "web_form_submit":      Tier.ASK,
    "web_fill_form":        Tier.ASK,
    "install_package":      Tier.ASK,
    "create_calendar_event": Tier.ASK,
    "browser_navigate":     Tier.ASK,

    # BLOCK — hardcoded, can NEVER be overridden via chat or config
    "access_credentials":   Tier.BLOCK,
    "sudo_command":         Tier.BLOCK,
    "rm_recursive_root":    Tier.BLOCK,
    "curl_pipe_bash":       Tier.BLOCK,
    "format_disk":          Tier.BLOCK,
    "write_raw_disk":       Tier.BLOCK,
    "fork_bomb":            Tier.BLOCK,
}

# These are HARDCODED and cannot be changed by user config or LLM
HARDCODED_BLOCKS = {
    "access_credentials",
    "sudo_command",
    "rm_recursive_root",
    "curl_pipe_bash",
    "format_disk",
    "write_raw_disk",
    "fork_bomb",
}


@dataclass
class ActionPreview:
    """Preview shown to user before any ASK-tier action executes."""
    action_type: str
    description: str
    details: dict
    reversible: bool
    risk_level: str  # "low" | "medium" | "high"
    tier: Tier

    def to_dict(self) -> dict:
        return {
            "action_type": self.action_type,
            "description": self.description,
            "details": self.details,
            "reversible": self.reversible,
            "risk_level": self.risk_level,
            "tier": self.tier.value,
        }

    def format_for_terminal(self) -> str:
        """Format the preview for terminal display."""
        risk_emoji = {"low": "🟢", "medium": "🟡", "high": "🔴"}.get(self.risk_level, "⚪")
        reversible_str = "✓ Reversible" if self.reversible else "✗ Irreversible"

        lines = [
            f"\n{'='*50}",
            f"⚠️  Permission Required",
            f"Action: {self.description}",
            f"Risk: {risk_emoji} {self.risk_level.upper()}  |  {reversible_str}",
        ]

        if self.details:
            lines.append("Details:")
            for k, v in self.details.items():
                lines.append(f"  {k}: {v}")

        lines.append(f"{'='*50}")
        return "\n".join(lines)


class PermissionEngine:
    """
    Central permission gatekeeper for DuckClaw.

    Every action passes through here before execution.
    Every action is logged to the audit trail.

    Approval callbacks are set by the active interface (dashboard WebSocket,
    Telegram bot, Discord bot, or terminal prompt).
    """

    def __init__(
        self,
        config: PermissionsConfig,
        db_path: str,
    ):
        self.config = config
        self.db_path = db_path

        # Rules are always loaded from DB — self.rules is never built from the
        # hardcoded Python dict at runtime. DEFAULT_RULES is only used for seeding.
        self.rules: dict[str, Tier] = {}

        self._db = sqlite3.connect(db_path, check_same_thread=False)
        self._db.row_factory = sqlite3.Row
        self._db.executescript(AUDIT_SCHEMA)
        self._db.executescript(RULES_SCHEMA)
        self._db.commit()

        # First-run: if table is empty, seed all factory defaults into DB
        self._seed_rules_if_empty()
        # Always load runtime rules from DB
        self._load_rules_from_db()

        # Approval callback — set by active interface
        # Signature: async (preview: ActionPreview) -> bool
        self._approval_callback: Optional[Callable] = None

        # Notification callback — set by active interface
        # Signature: async (message: str) -> None
        self._notify_callback: Optional[Callable] = None

    def set_approval_callback(self, callback: Callable):
        """Register the function that asks user for approval."""
        self._approval_callback = callback

    def set_notify_callback(self, callback: Callable):
        """Register the function that notifies user."""
        self._notify_callback = callback

    def _seed_rules_if_empty(self):
        """On first run, insert all factory defaults into the DB."""
        count = self._db.execute(
            "SELECT COUNT(*) FROM permission_rules"
        ).fetchone()[0]
        if count == 0:
            self._db.executemany(
                "INSERT OR IGNORE INTO permission_rules (action_type, tier) VALUES (?, ?)",
                [(action, tier.value) for action, tier in DEFAULT_RULES.items()],
            )
            self._db.commit()
            logger.info(f"Seeded {len(DEFAULT_RULES)} factory permission rules into DB")

    def _load_rules_from_db(self):
        """Load ALL rules from DB into self.rules. HARDCODED_BLOCKS always stay BLOCK."""
        self.rules = {}
        rows = self._db.execute(
            "SELECT action_type, tier FROM permission_rules"
        ).fetchall()
        for row in rows:
            try:
                self.rules[row["action_type"]] = Tier(row["tier"])
            except ValueError:
                pass
        # Safety: HARDCODED_BLOCKS can never be anything but BLOCK, regardless of DB
        for action in HARDCODED_BLOCKS:
            self.rules[action] = Tier.BLOCK
        logger.info(f"Loaded {len(self.rules)} permission rules from DB")

    def get_all_rules(self) -> list:
        """Return all rules from DB with metadata for the Settings UI."""
        rows = self._db.execute(
            "SELECT action_type, tier FROM permission_rules ORDER BY action_type"
        ).fetchall()
        result = []
        for row in rows:
            action_type = row["action_type"]
            current_tier = row["tier"]
            factory_tier = DEFAULT_RULES.get(action_type, Tier.ASK).value
            result.append({
                "action_type": action_type,
                "tier": current_tier,
                "default_tier": factory_tier,
                "is_custom": current_tier != factory_tier,
                "is_hardcoded": action_type in HARDCODED_BLOCKS,
            })
        return result

    def set_rule(self, action_type: str, tier_str: str) -> bool:
        """
        Update a rule in DB and in self.rules immediately.
        Returns False if hardcoded-blocked or tier is invalid.
        """
        if action_type in HARDCODED_BLOCKS:
            return False
        try:
            tier = Tier(tier_str)
        except ValueError:
            return False
        self._db.execute(
            "INSERT OR REPLACE INTO permission_rules (action_type, tier) VALUES (?, ?)",
            (action_type, tier.value),
        )
        self._db.commit()
        self.rules[action_type] = tier
        logger.info(f"Permission rule updated: {action_type} → {tier.value}")
        return True

    def reset_rule(self, action_type: str):
        """Reset a rule to its factory default in DB and self.rules."""
        if action_type in HARDCODED_BLOCKS:
            return
        factory = DEFAULT_RULES.get(action_type, Tier.ASK)
        self._db.execute(
            "INSERT OR REPLACE INTO permission_rules (action_type, tier) VALUES (?, ?)",
            (action_type, factory.value),
        )
        self._db.commit()
        self.rules[action_type] = factory
        logger.info(f"Permission rule reset to factory default: {action_type} → {factory.value}")

    def get_tier(self, action_type: str) -> Tier:
        """
        Get the permission tier for an action type.
        If the action is unknown, auto-register it as ASK in the DB so it
        appears in the Settings UI and can be customised.
        """
        if action_type in HARDCODED_BLOCKS:
            return Tier.BLOCK
        if action_type not in self.rules:
            # New / unknown action — save to DB with safe default (ASK)
            self._db.execute(
                "INSERT OR IGNORE INTO permission_rules (action_type, tier) VALUES (?, ?)",
                (action_type, Tier.ASK.value),
            )
            self._db.commit()
            self.rules[action_type] = Tier.ASK
            logger.info(f"Auto-registered unknown action '{action_type}' as ASK in DB")
        return self.rules[action_type]

    async def check(
        self,
        action_type: str,
        description: str,
        details: Optional[dict] = None,
        source: str = "system",
        session_id: Optional[str] = None,
        reversible: bool = True,
        risk_level: str = "low",
    ) -> bool:
        """
        Check permission for an action. Returns True if approved, False if denied/blocked.

        This is the main entry point. Call this before EVERY sensitive action.

        Example:
            approved = await permissions.check(
                action_type="screen_capture",
                description="Take a screenshot of your screen",
                details={"monitor": 0},
                reversible=True,
                risk_level="low",
            )
            if approved:
                do_screenshot()
        """
        tier = self.get_tier(action_type)
        logger.info(f"Permission check: action_type={action_type}, tier={tier}, description={description}, session_id={session_id}")

        if tier == Tier.SAFE:
            logger.info(f"SAFE-tier action auto-approved: action_type={action_type}, description={description}")
            status = "auto_approved"
            approved = True

        elif tier == Tier.NOTIFY:
            logger.info(f"NOTIFY-tier action auto-approved with user notification: action_type={action_type}, description={description}")
            approved = True
            status = "notified"
            if self._notify_callback:
                try:
                    logger.info("Invoking notify callback for NOTIFY-tier action")
                    await self._notify_callback(f"ℹ️ {description}")
                except Exception as e:
                    logger.warning(f"Notify callback failed: {e}")

        elif tier == Tier.ASK:
            logger.info(f"ASK-tier action requested: session_id={session_id}, action_type={action_type}, description={description}")
            preview = ActionPreview(
                action_type=action_type,
                description=description,
                details=details or {},
                reversible=reversible,
                risk_level=risk_level,
                tier=tier,
            )

            approved = False
            if self._approval_callback:
                logger.info("Invoking approval callback for ASK-tier action")
                try:
                    approved = await self._approval_callback(preview)
                    logger.info(f"approval callback completed and got = {approved} ")
                    logger.info(f"Approval callback returned: {'approved' if approved else 'denied'} for action_type={action_type}")
                except Exception as e:
                    logger.error(f"Approval callback failed: {e}")
                    approved = False
            else:
                # No callback set — fall back to terminal prompt
                logger.info("No approval callback set, falling back to terminal prompt for ASK-tier action")
                approved = await self._terminal_prompt(preview)


            status = "user_approved" if approved else "user_denied"
            logger.info(f"ASK-tier action {'approved' if approved else 'denied'}: session_id={session_id}, action_type={action_type}, description={description}")

        else:  # BLOCK
            logger.warning(f"BLOCK-tier action attempted and blocked: action_type={action_type}, description={description}")
            approved = False
            status = "blocked"
            if self._notify_callback:
                try:
                    await self._notify_callback(f"🚫 BLOCKED: {description}")
                except Exception:
                    pass
            logger.warning(f"BLOCKED action attempted: {action_type} — {description}")

        # Log EVERYTHING to audit trail
        self._log(
            action_type=action_type,
            tier=tier,
            description=description,
            details=details,
            status=status,
            source=source,
            session_id=session_id,
            reversible=reversible,
            risk_level=risk_level,
        )

        return approved

    async def _terminal_prompt(self, preview: ActionPreview) -> bool:
        """Fallback: ask for approval in the terminal."""
        import sys
        print(preview.format_for_terminal())

        loop = asyncio.get_event_loop()
        try:
            answer = await loop.run_in_executor(
                None,
                lambda: input("Approve? [y/N]: ").strip().lower()
            )
            logger.info(f"Terminal prompt received input: {answer}")
            return answer in ("y", "yes", 1, "1", "true", "t")
        except (EOFError, KeyboardInterrupt):
            return False

    def _log(
        self,
        action_type: str,
        tier: Tier,
        description: str,
        status: str,
        details: Optional[dict],
        source: str,
        session_id: Optional[str],
        reversible: bool,
        risk_level: str,
    ):
        """Write to audit log. Every action, no exceptions."""
        try:
            self._db.execute(
                "INSERT INTO audit_log "
                "(action_type, tier, description, details, status, source, session_id, reversible, risk_level) "
                "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
                (
                    action_type,
                    tier.value,
                    description,
                    json.dumps(details) if details else None,
                    status,
                    source,
                    session_id,
                    int(reversible),
                    risk_level,
                ),
            )
            self._db.commit()
        except Exception as e:
            logger.error(f"Failed to write audit log: {e}")

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

    def get_audit_log(
        self,
        limit: int = 50,
        offset: int = 0,
        action_type: Optional[str] = None,
        status: Optional[str] = None,
        tier: Optional[str] = None,
    ) -> list[dict]:
        """Fetch audit log entries for the dashboard."""
        conditions = []
        params = []

        if action_type:
            conditions.append("action_type = ?")
            params.append(action_type)
        if status:
            conditions.append("status = ?")
            params.append(status)
        if tier:
            conditions.append("tier = ?")
            params.append(tier)

        where = f"WHERE {' AND '.join(conditions)}" if conditions else ""
        params.extend([limit, offset])

        rows = self._db.execute(
            f"SELECT * FROM audit_log {where} ORDER BY timestamp DESC LIMIT ? OFFSET ?",
            params,
        ).fetchall()

        return [dict(r) for r in rows]

    def get_audit_stats(self) -> dict:
        """Summary stats for the dashboard."""
        total = self._db.execute("SELECT COUNT(*) FROM audit_log").fetchone()[0]
        approved = self._db.execute(
            "SELECT COUNT(*) FROM audit_log WHERE status IN ('auto_approved', 'notified', 'user_approved')"
        ).fetchone()[0]
        denied = self._db.execute(
            "SELECT COUNT(*) FROM audit_log WHERE status = 'user_denied'"
        ).fetchone()[0]
        blocked = self._db.execute(
            "SELECT COUNT(*) FROM audit_log WHERE status = 'blocked'"
        ).fetchone()[0]

        return {
            "total_actions": total,
            "approved": approved,
            "denied": denied,
            "blocked": blocked,
        }

    def export_audit_log(self, fmt: str = "json") -> str:
        """Export full audit log as JSON or CSV."""
        rows = self.get_audit_log(limit=100000)
        if fmt == "json":
            return json.dumps(rows, indent=2)
        elif fmt == "csv":
            if not rows:
                return "id,timestamp,action_type,tier,description,status,source\n"
            headers = list(rows[0].keys())
            lines = [",".join(headers)]
            for row in rows:
                values = [str(row.get(h, "")).replace(",", ";") for h in headers]
                lines.append(",".join(values))
            return "\n".join(lines)
        raise ValueError(f"Unknown format: {fmt}")

    def close(self):
        if self._db:
            self._db.close()
