"""
Azure Key Vault client for kvenv.

Uses Azure CLI (az) to fetch secrets with proper input validation
to prevent command injection attacks.
"""

from __future__ import annotations

import re
import shutil
import subprocess
from typing import NoReturn


class KeyVaultError(Exception):
    """Base exception for Key Vault operations."""

    pass


class AzureCliNotFoundError(KeyVaultError):
    """Raised when Azure CLI is not found."""

    pass


class KeyVaultAccessError(KeyVaultError):
    """Raised when there's an error accessing Key Vault."""

    pass


class InvalidVaultNameError(KeyVaultError):
    """Raised when the vault name is invalid."""

    pass


class InvalidSecretNameError(KeyVaultError):
    """Raised when the secret name is invalid."""

    pass


# Azure Key Vault naming rules:
# Vault name: 3-24 characters, alphanumeric and hyphens, must start with letter
# Secret name: 1-127 characters, alphanumeric and hyphens

# Pattern for valid vault names
# Must start with a letter, followed by alphanumeric or hyphens, 3-24 chars total
VAULT_NAME_PATTERN = re.compile(r"^[a-zA-Z][a-zA-Z0-9-]{2,23}$")

# Pattern for valid secret names
# Alphanumeric and hyphens, 1-127 chars
SECRET_NAME_PATTERN = re.compile(r"^[a-zA-Z0-9-]{1,127}$")


def validate_vault_name(vault_name: str) -> None:
    """
    Validate an Azure Key Vault name.

    Args:
        vault_name: The vault name to validate

    Raises:
        InvalidVaultNameError: If the vault name is invalid
    """
    if not vault_name:
        raise InvalidVaultNameError("Vault name cannot be empty")

    if not VAULT_NAME_PATTERN.match(vault_name):
        raise InvalidVaultNameError(
            f"Invalid vault name: '{vault_name}'. "
            "Vault names must be 3-24 characters, start with a letter, "
            "and contain only alphanumeric characters and hyphens."
        )


def validate_secret_name(secret_name: str) -> None:
    """
    Validate an Azure Key Vault secret name.

    Args:
        secret_name: The secret name to validate

    Raises:
        InvalidSecretNameError: If the secret name is invalid
    """
    if not secret_name:
        raise InvalidSecretNameError("Secret name cannot be empty")

    if not SECRET_NAME_PATTERN.match(secret_name):
        raise InvalidSecretNameError(
            f"Invalid secret name: '{secret_name}'. "
            "Secret names must be 1-127 characters and contain only "
            "alphanumeric characters and hyphens."
        )


def check_az_cli() -> None:
    """
    Check if Azure CLI is available.

    Raises:
        AzureCliNotFoundError: If az CLI is not found in PATH
    """
    if shutil.which("az") is None:
        raise AzureCliNotFoundError(
            "Azure CLI 'az' not found in PATH. "
            "Please install it from https://docs.microsoft.com/en-us/cli/azure/install-azure-cli"
        )


def fetch_secret(vault_name: str, secret_name: str) -> str:
    """
    Fetch a secret value from Azure Key Vault using az CLI.

    This function validates all inputs before executing the command
    to prevent command injection attacks.

    Args:
        vault_name: The name of the Key Vault
        secret_name: The name of the secret

    Returns:
        The secret value as a string

    Raises:
        InvalidVaultNameError: If the vault name is invalid
        InvalidSecretNameError: If the secret name is invalid
        AzureCliNotFoundError: If az CLI is not found
        KeyVaultAccessError: If there's an error fetching the secret
    """
    # Validate inputs to prevent injection
    validate_vault_name(vault_name)
    validate_secret_name(secret_name)

    # Check az CLI is available
    check_az_cli()

    # Build command as a list (not shell string) to prevent injection
    # subprocess.run with a list does NOT invoke a shell, so special
    # characters in arguments are not interpreted
    cmd = [
        "az",
        "keyvault",
        "secret",
        "show",
        "--vault-name",
        vault_name,
        "--name",
        secret_name,
        "--query",
        "value",
        "-o",
        "tsv",
    ]

    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            check=False,  # We'll handle errors ourselves for better messages
        )

        if result.returncode != 0:
            stderr = result.stderr.strip()

            # Check for common error patterns
            if "SecretNotFound" in stderr or "not found" in stderr.lower():
                raise KeyVaultAccessError(
                    f"Secret '{secret_name}' not found in vault '{vault_name}'"
                )
            elif "Forbidden" in stderr or "Access denied" in stderr:
                raise KeyVaultAccessError(
                    f"Access denied to secret '{secret_name}' in vault '{vault_name}'. "
                    "Check your permissions."
                )
            elif "VaultNotFound" in stderr:
                raise KeyVaultAccessError(f"Key Vault '{vault_name}' not found")
            elif "AADSTS" in stderr or "login" in stderr.lower():
                raise KeyVaultAccessError(
                    "Not authenticated to Azure. Please run 'az login' first."
                )
            else:
                raise KeyVaultAccessError(
                    f"Failed to fetch secret '{secret_name}' from vault '{vault_name}': {stderr}"
                )

        # Return the secret value, stripping any trailing newline
        return result.stdout.rstrip("\n")

    except FileNotFoundError:
        raise AzureCliNotFoundError("Azure CLI 'az' not found in PATH")
    except subprocess.SubprocessError as e:
        raise KeyVaultAccessError(f"Error executing az CLI: {e}")
