from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path
from typing import Set, Tuple, Optional

import os

try:  # Python 3.11+
    import tomllib
except ImportError:  # Python 3.8–3.10
    import tomli as tomllib

from .visuals import log


@dataclass
class PluginPolicy:
    mode: str = "allow_all"  # "allow_all" | "allowlist" | "blocklist"
    allow_unlisted_at_own_risk: bool = True
    require_explicit_ack_for_unlisted: bool = False
    strict_environment: bool = False

    allowed: Set[str] = field(default_factory=set)
    blocked: Set[str] = field(default_factory=set)
    core_commands: Set[str] = field(default_factory=set)

    config_path: Optional[Path] = None


def _default_config_path() -> Path:
    """
    Returns the default location for plugins.toml.
    Priority:
    - RGD_PLUGINS_CONFIG env var
    - ./plugins.toml
    - ~/.config/openrgd/plugins.toml
    """
    env_path = os.getenv("RGD_PLUGINS_CONFIG")
    if env_path:
        return Path(env_path).expanduser().resolve()

    cwd_path = Path("plugins.toml").resolve()
    if cwd_path.exists():
        return cwd_path

    config_home = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
    return (config_home / "openrgd" / "plugins.toml").resolve()


def load_plugin_policy(path: Optional[Path] = None) -> PluginPolicy:
    """
    Load plugin policy from the given path or from the default config path.

    If the file does not exist, returns a default policy with mode="allow_all"
    and no allowed/blocked lists, but still sets config_path to a sensible
    location for future writes.
    """
    if path is None:
        path = _default_config_path()

    policy = PluginPolicy()
    policy.config_path = path

    if not path.exists():
        # No config file: default permissive policy
        log(f"No plugins.toml found at {path}. Using default 'allow_all' policy.", "DEBUG")
        return policy

    try:
        data = tomllib.loads(path.read_text(encoding="utf-8"))
    except Exception as e:
        log(f"Failed to parse plugins.toml at {path}: {e}", "ERROR")
        return policy

    policy_data = data.get("policy", {})
    policy.mode = str(policy_data.get("mode", policy.mode))
    policy.allow_unlisted_at_own_risk = bool(
        policy_data.get("allow_unlisted_at_own_risk", policy.allow_unlisted_at_own_risk)
    )
    policy.require_explicit_ack_for_unlisted = bool(
        policy_data.get("require_explicit_ack_for_unlisted", policy.require_explicit_ack_for_unlisted)
    )
    policy.strict_environment = bool(
        policy_data.get("strict_environment", policy.strict_environment)
    )

    core_data = data.get("core", {})
    core_cmds = core_data.get("commands", [])
    policy.core_commands = set(str(c) for c in core_cmds)

    plugins_data = data.get("plugins", {})
    allowed_list = plugins_data.get("allowed", [])
    blocked_list = plugins_data.get("blocked", [])

    policy.allowed = set(str(x) for x in allowed_list)
    policy.blocked = set(str(x) for x in blocked_list)

    return policy


def save_plugin_policy(policy: PluginPolicy) -> None:
    """
    Persist the plugin policy back to plugins.toml.

    This function overwrites the file with a canonical structure.
    Comments will not be preserved.
    """
    path = policy.config_path or _default_config_path()
    path.parent.mkdir(parents=True, exist_ok=True)

    lines = []
    lines.append("# Auto-generated by OpenRGD plugin manager.")
    lines.append("# Edit with care or use 'rgd plugins' commands.")
    lines.append("")
    lines.append("[policy]")
    lines.append(f'mode = "{policy.mode}"')
    lines.append(f"allow_unlisted_at_own_risk = {str(policy.allow_unlisted_at_own_risk).lower()}")
    lines.append(
        "require_explicit_ack_for_unlisted = "
        f"{str(policy.require_explicit_ack_for_unlisted).lower()}"
    )
    lines.append(f"strict_environment = {str(policy.strict_environment).lower()}")
    lines.append("")
    lines.append("[core]")
    if policy.core_commands:
        core_sorted = sorted(policy.core_commands)
        core_vals = ", ".join(f'"{c}"' for c in core_sorted)
        lines.append(f"commands = [{core_vals}]")
    else:
        lines.append("commands = []")
    lines.append("")
    lines.append("[plugins]")
    allowed_sorted = sorted(policy.allowed)
    blocked_sorted = sorted(policy.blocked)

    allowed_vals = ", ".join(f'"{name}"' for name in allowed_sorted)
    blocked_vals = ", ".join(f'"{name}"' for name in blocked_sorted)

    lines.append(f"allowed = [{allowed_vals}]")
    lines.append(f"blocked = [{blocked_vals}]")
    lines.append("")

    path.write_text("\n".join(lines), encoding="utf-8")
    log(f"Plugin policy saved to {path}", "SUCCESS")


def evaluate_external_plugin(name: str, policy: PluginPolicy) -> Tuple[bool, str]:
    """
    Decide whether an external plugin should be loaded according to the policy.

    Returns:
        (is_allowed, status_label)
        status_label ∈ {"allowed", "blocked", "unlisted_allowed", "unlisted_blocked"}
    """
    mode = policy.mode or "allow_all"
    allow_unlisted = policy.allow_unlisted_at_own_risk
    if policy.strict_environment:
        allow_unlisted = False

    if mode == "allow_all":
        if name in policy.blocked:
            return False, "blocked"
        return True, "allowed"

    if mode == "blocklist":
        if name in policy.blocked:
            return False, "blocked"
        return True, "allowed"

    # mode == "allowlist"
    if name in policy.allowed:
        return True, "allowed"

    if allow_unlisted:
        return True, "unlisted_allowed"

    return False, "unlisted_blocked"
