"""Generate polkit and sudoers rules for mount management authorization.

polkit rules authorize `systemctl start/stop` (D-Bus → systemd).
sudoers rules authorize `sudo systemd-cryptsetup attach` (direct binary).
See docs/internals.md § "Why polkit + sudoers (hybrid authorization)".
"""

from __future__ import annotations

from dataclasses import dataclass
from textwrap import dedent

from ..config import Config, LocalVolume, MountConfig, RemoteVolume

# Both known paths where systemd-cryptsetup may be installed
CRYPTSETUP_PATHS = [
    "/usr/lib/systemd/systemd-cryptsetup",
    "/lib/systemd/systemd-cryptsetup",
]

# Install paths for generated auth rules
POLKIT_RULES_PATH = "/etc/polkit-1/rules.d/50-nbkp.rules"
SUDOERS_RULES_PATH = "/etc/sudoers.d/nbkp"


@dataclass(frozen=True)
class AuthRules:
    """Generated authorization rules for mount management."""

    polkit: str | None
    """polkit rules content, or None if no volumes have mount config."""

    sudoers: str | None
    """sudoers content, or None if no encrypted volumes."""


def generate_polkit_rules(
    mount_units: list[str],
    user: str,
) -> str:
    """Generate polkit rules content for mount management."""
    units_js = ", ".join(f'"{u}"' for u in sorted(mount_units))
    return dedent(f"""\
        // Generated by nbkp config setup-auth
        // Install to: {POLKIT_RULES_PATH}
        polkit.addRule(function(action, subject) {{
            if (action.id == "org.freedesktop.systemd1.manage-units" &&
                [{units_js}].indexOf(action.lookup("unit")) > -1 &&
                subject.user == "{user}") {{
                return polkit.Result.YES;
            }}
        }});
    """)


def generate_sudoers_rules(
    cryptsetup_commands: list[str],
    user: str,
) -> str:
    """Generate sudoers content for systemd-cryptsetup attach."""
    lines = [
        "# Generated by nbkp config setup-auth",
        f"# Install to: {SUDOERS_RULES_PATH}",
        f"# Use: sudo visudo -f {SUDOERS_RULES_PATH}",
        *[f"{user} ALL=(root) NOPASSWD: {cmd}" for cmd in sorted(cryptsetup_commands)],
    ]
    return "\n".join(lines) + "\n"


def _volume_mount_unit(vol: LocalVolume | RemoteVolume) -> str:
    """Derive the systemd mount unit name from a volume path.

    Simplified convention: strip leading /, replace / with -, append .mount.
    The real unit name is derived at runtime via `systemd-escape --path`,
    but this approximation is sufficient for auth rule generation.
    """
    path = vol.path.lstrip("/").replace("/", "-")
    return f"{path}.mount"


def _volume_mount_units(
    vol: LocalVolume | RemoteVolume,
    mount: MountConfig,
) -> list[str]:
    """Collect mount units for a volume (mount unit + cryptsetup service)."""
    return [
        _volume_mount_unit(vol),
        *(
            [f"systemd-cryptsetup@{mount.encryption.mapper_name}.service"]
            if mount.encryption is not None
            else []
        ),
    ]


def _volume_cryptsetup_commands(mount: MountConfig) -> list[str]:
    """Build sudoers command entries for a volume's encryption config (systemd)."""
    if mount.encryption is None:
        return []
    mapper = mount.encryption.mapper_name
    uuid = mount.device_uuid
    return [
        f"{cpath} attach {mapper} /dev/disk/by-uuid/{uuid} /dev/stdin luks"
        for cpath in CRYPTSETUP_PATHS
    ]


def _direct_sudoers_commands(
    vol: LocalVolume | RemoteVolume,
    mount: MountConfig,
) -> list[str]:
    """Build sudoers command entries for the direct mount strategy.

    Covers mount, umount, and (if encrypted) cryptsetup open/close.
    The mount command uses only the path — device and options come from
    fstab (mirroring how the systemd strategy reads from the unit file).
    """
    mapper = mount.encryption.mapper_name if mount.encryption is not None else None
    return [
        f"/usr/bin/mount {vol.path}",
        f"/usr/bin/umount {vol.path}",
        *(
            [
                f"/usr/sbin/cryptsetup open --type luks"
                f" /dev/disk/by-uuid/{mount.device_uuid} {mapper} -",
                f"/usr/sbin/cryptsetup close {mapper}",
            ]
            if mapper
            else []
        ),
    ]


def generate_auth_rules(config: Config, user: str) -> AuthRules:
    """Generate polkit and sudoers rules for all volumes with mount config.

    Scans the config for volumes with mount config and builds the
    authorization rules that need to be installed on the target host.

    Strategy dispatch:
    - ``systemd`` or ``auto``: polkit for systemctl, sudoers for
      systemd-cryptsetup (existing behavior)
    - ``direct``: sudoers for mount/umount/cryptsetup (no polkit)
    """
    volumes_with_mount = [
        (vol, mount)
        for vol in config.volumes.values()
        if (mount := getattr(vol, "mount", None)) is not None
    ]

    systemd_volumes = [
        (vol, mount)
        for vol, mount in volumes_with_mount
        if mount.strategy in ("auto", "systemd")
    ]
    direct_volumes = [
        (vol, mount) for vol, mount in volumes_with_mount if mount.strategy == "direct"
    ]

    # Systemd auth rules
    mount_units = [
        unit
        for vol, mount in systemd_volumes
        for unit in _volume_mount_units(vol, mount)
    ]
    systemd_cryptsetup_commands = [
        cmd
        for _, mount in systemd_volumes
        for cmd in _volume_cryptsetup_commands(mount)
    ]

    # Direct auth rules
    direct_commands = [
        cmd
        for vol, mount in direct_volumes
        for cmd in _direct_sudoers_commands(vol, mount)
    ]

    all_sudoers_commands = [*systemd_cryptsetup_commands, *direct_commands]

    return AuthRules(
        polkit=(generate_polkit_rules(mount_units, user) if mount_units else None),
        sudoers=(
            generate_sudoers_rules(all_sudoers_commands, user)
            if all_sudoers_commands
            else None
        ),
    )
