from pathlib import Path, PurePath
from typing import Optional

import safe_radix32
from clarabot_nano_common.utility.path.common import ensure_path, normalize_validate_absolute_path
from clarabot_nano_common.utility.path.resolve import resolve_path
from clarabot_nano_common.utility.path.restrict import PathRestrictionValidator
from clarabot_nano_common.utility.path.sensitive import (
    normalize_validate_sensitive_absolute_path,
    SensitiveValidationError,
)

from clarabot_nano.core.config.update.base import AbstractRemoteConfigUpdateHandler, RemoteConfigUpdateError
from clarabot_nano.core.config.utility import block_name_from_config_format


class AbstractDriveUpdateHandler(AbstractRemoteConfigUpdateHandler):
    def _execute(self, data: dict):
        drive = self.get_drive_by_raw_path(data["path"])
        return self._execute_with_drive(drive, data)

    def _execute_with_drive(self, drive: Optional[dict], data: dict):
        raise NotImplementedError()

    def get_drive_by_raw_path(self, p: str) -> dict:
        for drive in self._c["drives"]:
            if drive["path"] == p:
                return drive

    def get_drive_by_path(self, p: Path) -> dict:
        for drive in self._c["drives"]:
            if PurePath(drive["path"]) == p:
                return drive

    def _describe_change(self, old_config: dict, new_config: dict, data: dict) -> str:
        raise NotImplementedError()


class UpdateDriveCreate(AbstractDriveUpdateHandler):
    HANDLER_KEY = "drive:create"

    def _execute_with_drive(self, drive: Optional[dict], data: dict):
        if drive:
            raise Exception("Drive already exists for path")  # drive for path already exists
        raw_path = ensure_path(data["path"])
        validator_fn = (
            normalize_validate_sensitive_absolute_path
            if self._local_c["restrict_sensitive_path_access"]
            else normalize_validate_absolute_path
        )
        drive_validator = PathRestrictionValidator(self._local_c["drive_restriction_paths"])
        try:
            root_path = Path(validator_fn(raw_path))
            drive_validator(root_path)
            root_path = resolve_path(root_path, strict=True)
            validator_fn(root_path)  # relevant for sensitive path checking post symlink resolution
            drive_validator(root_path)
            if not root_path.is_dir():
                raise Exception("Path is not a directory")
        except SensitiveValidationError as e:
            raise RemoteConfigUpdateError("sensitive") from e
        drive = self.get_drive_by_path(raw_path)
        if drive:
            raise Exception("Drive already exists for path")  # drive for path already exists
        new_drive = {
            "path": str(raw_path),  # config must work with non-posix paths as well, so just store native
        }
        self._c["drives"].append(new_drive)

    def _describe_change(self, old_config: dict, new_config: dict, data: dict) -> str:
        return f"Created drive for path {data.get('path')!r}"


class UpdateDriveDelete(AbstractDriveUpdateHandler):
    HANDLER_KEY = "drive:delete"

    def _execute_with_drive(self, drive: Optional[dict], data: dict):
        if not drive:
            raise Exception("Drive does not exist for path")  # drive does not exist
        self._c["drives"].remove(drive)

    def _describe_change(self, old_config: dict, new_config: dict, data: dict) -> str:
        return f"Deleted drive of path {data.get('path')!r}"


class UpdateDriveAttachRoom(AbstractDriveUpdateHandler):
    HANDLER_KEY = "drive:attach-room"

    def _execute_with_drive(self, drive: Optional[dict], data: dict):
        if not drive:
            raise Exception("Drive does not exist for path")  # drive does not exist
        room_id = data.get("room_id")
        try:
            room_id_int = safe_radix32.decode(room_id)
        except Exception as e:
            raise Exception("Room id is missing or invalid") from e
        bound_room_id_ints = set()
        for d in self._c["drives"]:
            for room in d.get("rooms", []):
                try:
                    bound_room_id_ints.add(safe_radix32.decode(room))
                except Exception:
                    pass
        if room_id_int in bound_room_id_ints:
            raise Exception("Room already bound for a drive on this Nano")
        room_blocks = data.get("room_blocks")
        if not isinstance(room_blocks, list):
            raise Exception("Room block(s) is missing or invalid")
        for room_block in room_blocks:
            if not block_name_from_config_format(room_block):
                raise Exception("Format of room block is invalid")
        drive.setdefault("rooms", []).append(room_id)
        self._c.setdefault("room_blocks", {})[room_id] = room_blocks

    def _describe_change(self, old_config: dict, new_config: dict, data: dict) -> str:
        return f"Room {data.get('room_id')!r} attached to drive at {data.get('path')!r}"


class UpdateDriveDetachRoom(AbstractDriveUpdateHandler):
    HANDLER_KEY = "drive:detach-room"

    def _execute_with_drive(self, drive: Optional[dict], data: dict):
        if not drive:
            raise Exception("Drive does not exist for path")  # drive does not exist
        try:
            drive.get("rooms", []).remove(data["room_id"])
        except ValueError:
            raise Exception("Room does not exist for drive")

    def _describe_change(self, old_config: dict, new_config: dict, data: dict) -> str:
        return f"Room {data.get('room_id')!r} detached from drive at {data.get('path')!r}"


class UpdateDriveAddBackupRoom(AbstractDriveUpdateHandler):
    HANDLER_KEY = "drive:add-backup-room"

    def _execute_with_drive(self, drive: Optional[dict], data: dict):
        if not drive:
            raise Exception("Drive does not exist for path")  # drive does not exist
        room_id = data.get("room_id")
        try:
            room_id_int = safe_radix32.decode(room_id)
        except Exception as e:
            raise Exception("Room id is missing or invalid") from e
        backup_room_id_ints = set()
        for d in self._c["drives"]:
            for room in d.get("backups", []):
                try:
                    backup_room_id_ints.add(safe_radix32.decode(room))
                except Exception:
                    pass
        if room_id_int in backup_room_id_ints:
            raise Exception("Room already set for backup on this Nano")
        drive.setdefault("backups", []).append(room_id)

    def _describe_change(self, old_config: dict, new_config: dict, data: dict) -> str:
        return f"Room {data.get('room_id')!r} added for backup in drive at {data.get('path')!r}"


class UpdateDriveDelBackupRoom(AbstractDriveUpdateHandler):
    HANDLER_KEY = "drive:del-backup-room"

    def _execute_with_drive(self, drive: Optional[dict], data: dict):
        if not drive:
            raise Exception("Drive does not exist for path")  # drive does not exist
        try:
            drive.get("backups", []).remove(data["room_id"])
        except ValueError:
            raise Exception("Backup does not exist for drive")

    def _describe_change(self, old_config: dict, new_config: dict, data: dict) -> str:
        return f"Room {data.get('room_id')!r} removed from backup in drive at {data.get('path')!r}"
