"""Exception hierarchy for Kuberoku.

The CLI catches KuberokuError at the top level and prints a friendly message.
Non-KuberokuError exceptions are unexpected bugs and show a traceback.
"""


class KuberokuError(Exception):
    """Base exception for all Kuberoku errors."""


# ── App errors ──────────────────────────────────────────────────────


class AppNotFoundError(KuberokuError):
    """Raised when an app doesn't exist."""

    def __init__(self, name: str) -> None:
        super().__init__(f"App '{name}' not found.")
        self.name = name


class AppAlreadyExistsError(KuberokuError):
    """Raised when creating an app that already exists."""

    def __init__(self, name: str) -> None:
        super().__init__(f"App '{name}' already exists.")
        self.name = name


# ── Validation errors ──────────────────────────────────────────────


class InvalidAppNameError(KuberokuError):
    """Raised when an app name fails validation."""

    def __init__(self, name: str, reason: str) -> None:
        super().__init__(f"Invalid app name '{name}': {reason}")
        self.name = name
        self.reason = reason


# ── Deploy errors ───────────────────────────────────────────────────


class DeployError(KuberokuError):
    """Raised when a deployment fails."""


class RolloutTimeoutError(DeployError):
    """Raised when a rollout doesn't complete in time."""


class ImageNotFoundError(DeployError):
    """Raised when the specified image can't be pulled."""


# ── Process errors ──────────────────────────────────────────────────


class ProcessTypeNotFoundError(KuberokuError):
    """Raised when a process type doesn't exist."""

    def __init__(self, process_type: str, app: str) -> None:
        super().__init__(f"Process type '{process_type}' not found on app '{app}'.")
        self.process_type = process_type
        self.app = app


# ── Build errors ────────────────────────────────────────────────────


class BuildError(KuberokuError):
    """Raised when a git/docker build fails."""


# ── Registry errors ─────────────────────────────────────────────────


class RegistryError(KuberokuError):
    """Raised when registry detection or push fails."""


# ── Config errors ───────────────────────────────────────────────────


class ConfigVarNotFoundError(KuberokuError):
    """Raised when a config var doesn't exist."""

    def __init__(self, key: str, app: str) -> None:
        super().__init__(f"Config var '{key}' not found on app '{app}'.")
        self.key = key
        self.app = app


class ConfigLimitError(KuberokuError):
    """Raised when a config operation exceeds a limit."""


class InvalidConfigKeyError(KuberokuError):
    """Raised when a config key fails validation."""

    def __init__(self, key: str, reason: str) -> None:
        super().__init__(f"Invalid config key '{key}': {reason}")
        self.key = key
        self.reason = reason


# ── Release errors ──────────────────────────────────────────────────


class ReleaseNotFoundError(KuberokuError):
    """Raised when a release version doesn't exist."""


# ── Cluster errors ──────────────────────────────────────────────────


class ClusterNotFoundError(KuberokuError):
    """Raised when a cluster name isn't configured."""

    def __init__(self, name: str) -> None:
        super().__init__(f"Cluster '{name}' not found in configuration.")
        self.name = name


class ClusterAlreadyExistsError(KuberokuError):
    """Raised when adding a cluster that already exists."""

    def __init__(self, name: str) -> None:
        super().__init__(f"Cluster '{name}' already exists.")
        self.name = name


class ClusterUnreachableError(KuberokuError):
    """Raised when the K8s API server can't be reached."""


class NoCurrentClusterError(KuberokuError):
    """Raised when no current cluster is set and none can be inferred."""

    def __init__(self) -> None:
        super().__init__(
            "No current cluster. Use 'clusters:add' to register a cluster "
            "or 'clusters:switch' to select one."
        )


# ── Permission errors ───────────────────────────────────────────────


class PermissionDeniedError(KuberokuError):
    """Raised when the K8s user lacks required RBAC permissions."""

    def __init__(self, verb: str, resource: str, suggestion: str = "") -> None:
        msg = f"Permission denied: cannot {verb} {resource}."
        if suggestion:
            msg += f"\n{suggestion}"
        super().__init__(msg)
        self.verb = verb
        self.resource = resource


# ── Addon errors ────────────────────────────────────────────────────


class AddonNotFoundError(KuberokuError):
    """Raised when an addon doesn't exist."""

    def __init__(self, instance_name: str, app: str) -> None:
        super().__init__(f"Addon '{instance_name}' not found on app '{app}'.")
        self.instance_name = instance_name
        self.app = app


class AddonAlreadyExistsError(KuberokuError):
    """Raised when creating an addon that already exists."""

    def __init__(self, instance_name: str, app: str) -> None:
        super().__init__(
            f"Addon '{instance_name}' already exists on app '{app}'. "
            f"Use --as to give it a different name."
        )
        self.instance_name = instance_name
        self.app = app


class AddonTypeNotSupportedError(KuberokuError):
    """Raised when an unsupported addon type is requested."""


# ── Networking errors ──────────────────────────────────────────


class AmbiguousNameError(KuberokuError):
    """Raised when a name matches both a process type and an addon."""

    def __init__(self, name: str, app: str) -> None:
        super().__init__(
            f"'{name}' matches both a process type and an addon on app '{app}'. "
            f"Use --process or --addon to disambiguate."
        )
        self.name = name
        self.app = app


class NameNotFoundError(KuberokuError):
    """Raised when a name matches neither a process type nor an addon."""

    def __init__(self, name: str, app: str) -> None:
        super().__init__(f"'{name}' is not a process type or addon on app '{app}'.")
        self.name = name
        self.app = app


class NotExposedError(KuberokuError):
    """Raised when trying to unexpose something that isn't exposed."""

    def __init__(self, name: str, app: str) -> None:
        super().__init__(f"'{name}' on app '{app}' is not currently exposed.")
        self.name = name
        self.app = app


class NoPortsError(KuberokuError):
    """Raised when a process/addon has no ports defined."""

    def __init__(self, name: str, app: str) -> None:
        super().__init__(f"'{name}' on app '{app}' has no ports defined.")
        self.name = name
        self.app = app


class PortAlreadyExistsError(KuberokuError):
    """Raised when adding a port that already exists."""

    def __init__(self, port: int, protocol: str, process_type: str, app: str) -> None:
        super().__init__(
            f"Port {port}/{protocol.lower()} already exists on '{process_type}' (app '{app}')."
        )
        self.port = port
        self.protocol = protocol
        self.process_type = process_type
        self.app = app


class PortNotFoundError(KuberokuError):
    """Raised when removing a port that doesn't exist."""

    def __init__(self, port: int, protocol: str, process_type: str, app: str) -> None:
        super().__init__(
            f"Port {port}/{protocol.lower()} not found on '{process_type}' (app '{app}')."
        )
        self.port = port
        self.protocol = protocol
        self.process_type = process_type
        self.app = app


class AmbiguousProcessTypeError(KuberokuError):
    """Raised when --type is required but not provided (multiple process types)."""

    def __init__(self, app: str, types: tuple[str, ...]) -> None:
        super().__init__(
            f"App '{app}' has multiple process types: {', '.join(types)}. Use --type to specify."
        )
        self.app = app
        self.types = types


# ── Runtime errors (logs/exec) ─────────────────────────────────────


class NoDynosError(KuberokuError):
    """Raised when no running pods exist for a process type."""

    def __init__(self, process_type: str, app: str) -> None:
        super().__init__(f"No running dynos for process type '{process_type}' on app '{app}'.")
        self.process_type = process_type
        self.app = app


class DynoNotFoundError(KuberokuError):
    """Raised when a specific dyno doesn't exist."""

    def __init__(self, dyno: str, app: str) -> None:
        super().__init__(f"Dyno '{dyno}' not found on app '{app}'.")
        self.dyno = dyno
        self.app = app


class InvalidDurationError(KuberokuError):
    """Raised when a --since duration value is invalid."""

    def __init__(self, text: str) -> None:
        super().__init__(f"Invalid duration '{text}'. Use format like '5m', '1h', '30s', '2h30m'.")
        self.text = text


# ── Domain errors ───────────────────────────────────────────────────


class DomainAlreadyExistsError(KuberokuError):
    """Raised when a domain is already configured."""

    def __init__(self, hostname: str, app: str) -> None:
        super().__init__(f"Domain '{hostname}' already exists on app '{app}'.")
        self.hostname = hostname
        self.app = app


class DomainNotFoundError(KuberokuError):
    """Raised when a domain doesn't exist on an app."""

    def __init__(self, hostname: str, app: str) -> None:
        super().__init__(f"Domain '{hostname}' not found on app '{app}'.")
        self.hostname = hostname
        self.app = app


class AutoDomainRemoveError(KuberokuError):
    """Raised when trying to remove an auto-generated domain."""

    def __init__(self, hostname: str) -> None:
        super().__init__(
            f"Cannot remove auto-generated domain '{hostname}'. Use 'domains:clear' instead."
        )
        self.hostname = hostname


class IngressControllerNotFoundError(KuberokuError):
    """Raised when no Ingress controller is detected."""

    def __init__(self) -> None:
        super().__init__(
            "No Ingress controller detected. "
            "Run 'clusters:setup' or install one (nginx-ingress, traefik, etc.)."
        )


class CertManagerNotFoundError(KuberokuError):
    """Raised when cert-manager is needed but not detected."""

    def __init__(self) -> None:
        super().__init__(
            "cert-manager not found. Use '--no-tls' to skip TLS, "
            "or install cert-manager for automatic certificates."
        )


class GatewayPortExhaustedError(KuberokuError):
    """Raised when the gateway port range is exhausted."""

    def __init__(self) -> None:
        super().__init__(
            "Gateway port range exhausted (10000-10999). "
            "Deallocate unused ports or use dedicated LoadBalancers."
        )


# ── Plugin errors ───────────────────────────────────────────────────


class PluginError(KuberokuError):
    """Raised when a plugin fails to load."""
