from __future__ import annotations
import re
from pathlib import Path
import typer
from acc_cli.config import TEMPLATES

# acc command validators

def validate_project_name(value: str) -> str:
    """
    Project name must be 2–64 characters, start with a letter,
    and contain only letters, digits, spaces, hyphens, and underscores.
    """
    value = value.strip()

    if not value:
        raise typer.BadParameter("Project name cannot be empty.")

    if len(value) < 2:
        raise typer.BadParameter("Project name must be at least 2 characters long.")

    if len(value) > 64:
        raise typer.BadParameter("Project name must be 64 characters or fewer.")

    if not value[0].isalpha():
        raise typer.BadParameter("Project name must start with a letter.")

    if not re.fullmatch(r"[A-Za-z][A-Za-z0-9 _\-]*", value):
        raise typer.BadParameter(
            "Project name may only contain letters, digits, spaces, hyphens (-), "
            "and underscores (_)."
        )

    return value


def validate_version(value: str) -> str:
    """
    Version must follow PEP 440 / SemVer: MAJOR.MINOR.PATCH (e.g. "0.1.0").
    Each part must be a non-negative integer.
    """
    value = value.strip()

    if not value:
        raise typer.BadParameter("Version cannot be empty.")

    if not re.fullmatch(r"\d+\.\d+\.\d+", value):
        raise typer.BadParameter(
            f"Invalid version '{value}'. Expected MAJOR.MINOR.PATCH format (e.g. 0.1.0)."
        )

    return value

def validate_author(value: str) -> str:
    """
    Author must be a non-empty string, 2–100 characters.
    Allows letters, spaces, hyphens, apostrophes, and dots
    """
    value = value.strip()

    if not value:
        raise typer.BadParameter("Author cannot be empty.")

    if len(value) < 2:
        raise typer.BadParameter("Author must be at least 2 characters long.")

    if len(value) > 100:
        raise typer.BadParameter("Author must be 100 characters or fewer.")

    return value


def validate_template(value: str) -> str:
    """
    Template must be one of the keys defined in config.TEMPLATES.
    """
    value = value.strip()

    if not value:
        raise typer.BadParameter("Template cannot be empty.")

    if value not in TEMPLATES:
        allowed = " / ".join(TEMPLATES.keys())
        raise typer.BadParameter(
            f"Unknown template '{value}'. Choose one of: {allowed}"
        )

    return value


def validate_etl_type(value: str, template: str) -> str:
    """
    ETL type must be one of the types listed for the chosen template.
    Only called when the selected template actually has etl_types.

    """
    value = value.strip()

    allowed_types: list[str] = TEMPLATES[template].get("etl_types", [])

    if not value:
        raise typer.BadParameter("ETL type cannot be empty.")

    if value not in allowed_types:
        allowed = " / ".join(allowed_types)
        raise typer.BadParameter(
            f"Unknown ETL type '{value}' for template '{template}'. "
            f"Choose one of: {allowed}"
        )

    return value


def validate_output_dir(value: str) -> str:
    """
    Output directory must be a valid, writable path.
    The directory is created automatically if it does not yet exist
    """
    value = value.strip()

    if not value:
        raise typer.BadParameter("Output directory cannot be empty.")

    path = Path(value).expanduser().resolve()

    if path.exists() and not path.is_dir():
        raise typer.BadParameter(
            f"'{path}' already exists and is a file, not a directory."
        )

    check = path if path.exists() else next(
        (p for p in path.parents if p.exists()), Path("/")
    )
    if not _is_writable(check):
        raise typer.BadParameter(
            f"No write permission for output directory: {path}"
        )

    return value


# build command validators

WHEEL_DUMP_TARGETS: tuple[str, ...] = ("AZURE", "DBFS", "UC", "S3")

def validate_dump_target(value: str | None) -> str | None:
    """
    Dump target (--dump-target flag) must be one of WHEEL_DUMP_TARGETS
    when provided.  None is allowed — the command will prompt interactively.

    Normalises to uppercase and strips whitespace before comparison.

    Examples
    --------
    OK  : None, "S3", "dbfs", " UC "
    BAD : "", "gcs", "azure-blob"
    """
    if value is None:
        return None

    normalised = value.strip().upper()

    if not normalised:
        raise typer.BadParameter(
            f"Dump target cannot be blank. Choose one of: {' / '.join(WHEEL_DUMP_TARGETS)}"
        )

    if normalised not in WHEEL_DUMP_TARGETS:
        raise typer.BadParameter(
            f"Invalid dump target '{value}'. "
            f"Choose one of: {' / '.join(WHEEL_DUMP_TARGETS)}"
        )

    return normalised


def validate_s3_bucket(value: str) -> str:
    """
    S3 bucket name rules (AWS enforced):
    - 3–63 characters
    - Lowercase letters, digits, and hyphens only
    - Must start and end with a letter or digit
    - Cannot be formatted as an IP address

    Examples
    --------
    OK  : "my-bucket", "data-lake-prod", "abc123"
    BAD : "", "My-Bucket", "-bucket", "bucket-", "192.168.1.1"
    """
    value = value.strip()

    if not value:
        raise typer.BadParameter("S3 bucket name cannot be empty.")

    if len(value) < 3 or len(value) > 63:
        raise typer.BadParameter(
            f"S3 bucket name must be 3–63 characters long (got {len(value)})."
        )

    if not re.fullmatch(r"[a-z0-9][a-z0-9\-]*[a-z0-9]", value):
        raise typer.BadParameter(
            "S3 bucket name must use only lowercase letters, digits, and hyphens, "
            "and must start and end with a letter or digit."
        )

    # Reject IP-address-style names (AWS rejects these too)
    if re.fullmatch(r"\d+\.\d+\.\d+\.\d+", value):
        raise typer.BadParameter(
            "S3 bucket name cannot be formatted as an IP address."
        )

    return value


def validate_s3_folder(value: str) -> str:
    """
    S3 folder / prefix:
    - May be empty (uploads to bucket root).
    - Must not start or end with '/'.
    - No consecutive slashes.
    - Allowed characters: letters, digits, hyphens, underscores, dots, slashes.
    """
    value = value.strip()

    if not value:
        return value  # empty folder is valid — uploads to bucket root

    if value.startswith("/"):
        raise typer.BadParameter("S3 folder must not start with '/'.")

    if value.endswith("/"):
        raise typer.BadParameter("S3 folder must not end with '/'.")

    if "//" in value:
        raise typer.BadParameter("S3 folder must not contain consecutive slashes (//).")

    if not re.fullmatch(r"[A-Za-z0-9_.$/\-]+", value):
        raise typer.BadParameter(
            "S3 folder may only contain letters, digits, hyphens, underscores, "
            "dots, slashes, and dollar signs."
        )

    return value


def validate_uc_volume_path(value: str) -> str:
    """
    Unity Catalog volume path must follow the format:
        /Volumes/<catalog>/<schema>/<volume>

    All three path components are required and must be non-empty identifiers
    (letters, digits, underscores only — matching UC naming rules).

    Examples
    --------
    OK  : "/Volumes/workspace/default/volume1", "/Volumes/my_cat/etl_schema/whl_vol"
    BAD : "", "/volumes/x/y/z" (wrong case), "/Volumes/x/y" (missing volume),
          "/Volumes/x/y/z/extra" (too deep)
    """
    value = value.strip()

    if not value:
        raise typer.BadParameter("UC Volume path cannot be empty.")

    if not value.startswith("/Volumes/"):
        raise typer.BadParameter(
            f"UC Volume path must start with '/Volumes/' (got '{value}'). "
            "Expected format: /Volumes/<catalog>/<schema>/<volume>"
        )

    # Strip leading "/Volumes/" and split the remainder
    remainder = value[len("/Volumes/"):]
    parts = remainder.split("/")

    if len(parts) != 3:
        raise typer.BadParameter(
            f"UC Volume path must have exactly 3 components after '/Volumes/' "
            f"(catalog/schema/volume), got {len(parts)}: '{value}'. "
            "Expected format: /Volumes/<catalog>/<schema>/<volume>"
        )

    identifier_re = re.compile(r"^[A-Za-z0-9_]+$")
    labels = ("catalog", "schema", "volume")

    for label, part in zip(labels, parts):
        if not part:
            raise typer.BadParameter(
                f"The {label} component of the UC Volume path cannot be empty."
            )
        if not identifier_re.match(part):
            raise typer.BadParameter(
                f"Invalid {label} name '{part}' in UC Volume path. "
                "Use only letters, digits, and underscores."
            )

    return value


def validate_adls_destination(value: str) -> str:
    """
    ADLS Gen2 destination must follow the abfss:// scheme:
        abfss://<container>@<account>.dfs.core.windows.net/<path>

    Checks:
    - Correct scheme prefix
    - Non-empty container name
    - Account host contains '.dfs.core.windows.net'
    - At least one path component after the host

    Examples
    --------
    OK  : "abfss://mycontainer@myaccount.dfs.core.windows.net/wheels"
    BAD : "", "https://...", "abfss://container@account/path" (missing .dfs host)
    """
    value = value.strip()

    if not value:
        raise typer.BadParameter("ADLS destination cannot be empty.")

    if not value.startswith("abfss://"):
        raise typer.BadParameter(
            f"ADLS destination must start with 'abfss://' (got '{value}')."
        )

    without_scheme = value[len("abfss://"):]

    if "@" not in without_scheme:
        raise typer.BadParameter(
            "ADLS destination must include '@' separating the container from the account host. "
            "Expected: abfss://<container>@<account>.dfs.core.windows.net/<path>"
        )

    container, rest = without_scheme.split("@", 1)

    if not container:
        raise typer.BadParameter("ADLS container name cannot be empty.")

    if "/" not in rest:
        raise typer.BadParameter(
            "ADLS destination must include a path after the account host. "
            "Expected: abfss://<container>@<account>.dfs.core.windows.net/<path>"
        )

    account_host, remote_path = rest.split("/", 1)

    if ".dfs.core.windows.net" not in account_host:
        raise typer.BadParameter(
            f"ADLS account host '{account_host}' does not look valid. "
            "Expected format: <account>.dfs.core.windows.net"
        )

    if not remote_path.strip("/"):
        raise typer.BadParameter(
            "ADLS remote path cannot be empty. Provide a path after the host, "
            "e.g. abfss://container@account.dfs.core.windows.net/wheels"
        )

    return value

def validate_aws_access_key_id(value: str) -> str:
    """
    AWS Access Key IDs begin with 'AKIA' or 'ASIA' and are exactly 20
    uppercase alphanumeric characters.
    """
    value = value.strip()

    if not value:
        raise typer.BadParameter("AWS Access Key ID cannot be empty.")

    if not re.fullmatch(r"[A-Z0-9]{20}", value):
        raise typer.BadParameter(
            "AWS Access Key ID must be exactly 20 uppercase letters/digits "
            "(e.g. AKIAIOSFODNN7EXAMPLE)."
        )

    return value
def validate_aws_secret_access_key(value: str) -> str:
    """
    AWS Secret Access Keys are exactly 40 characters and contain only
    base64-compatible characters: letters, digits, '+', '/', and '='.
    """
    value = value.strip()

    if not value:
        raise typer.BadParameter("AWS Secret Access Key cannot be empty.")

    if len(value) != 40:
        raise typer.BadParameter(
            f"AWS Secret Access Key must be exactly 40 characters (got {len(value)})."
        )

    if not re.fullmatch(r"[A-Za-z0-9+/=]{40}", value):
        raise typer.BadParameter(
            "AWS Secret Access Key may only contain letters, digits, '+', '/', and '='."
        )

    return value
def validate_aws_region(value: str) -> str:
    """
    AWS region must follow the standard pattern: <area>-<direction>-<number>
    """
    value = value.strip()

    if not value:
        raise typer.BadParameter("AWS Region cannot be empty.")

    if not re.fullmatch(r"[a-z]+-[a-z]+-\d", value):
        raise typer.BadParameter(
            f"Invalid AWS region '{value}'. "
            "Expected format: <area>-<direction>-<number> (e.g. us-east-1)."
        )

    return value

def validate_databricks_host(value: str) -> str:
    """
    Databricks workspace host must be an HTTPS URL.
    """
    value = value.strip()

    if not value:
        raise typer.BadParameter("Databricks host cannot be empty.")

    if not value.startswith("https://"):
        raise typer.BadParameter(
            f"Databricks host must start with 'https://' (got '{value}')."
        )

    host_part = value[len("https://"):]
    if not host_part:
        raise typer.BadParameter("Databricks host URL is incomplete.")

    return value.rstrip("/")  # normalise: strip trailing slash

def validate_databricks_token(value: str) -> str:
    """
    Databricks PAT tokens start with 'dapi' and are at least 32 characters.
    """
    value = value.strip()

    if not value:
        raise typer.BadParameter("Databricks PAT token cannot be empty.")

    if not value.startswith("dapi"):
        raise typer.BadParameter(
            "Databricks PAT token must start with 'dapi' "
            "(generate one at Workspace → Settings → Developer → Access tokens)."
        )

    if len(value) < 32:
        raise typer.BadParameter(
            "Databricks PAT token looks too short (minimum 32 characters). "
            "Make sure you copied the full token."
        )

    return value

def _is_writable(path: Path) -> bool:
    """Return True if the process has write access to path."""
    import os
    return os.access(path, os.W_OK)