"""``acc build`` — produce a wheel, upload it to the destination chosen by the user,
and write a manifest.
"""

from __future__ import annotations
import json
import logging
import subprocess
import sys
import tomllib
from pathlib import Path
import typer
from acc_cli.utils.authenticator import (
    get_s3_credentials,
    get_databricks_uc_credentials,
    invalidate_s3,
    invalidate_s3_session_token,
    invalidate_databricks_uc,
    invalidate_azure,
    get_azure_credentials
)
from acc_cli.utils.validators import (
    validate_dump_target,
    validate_s3_bucket,
    validate_s3_folder,
    validate_uc_volume_path,
    validate_adls_destination,
)
from acc_cli.utils.utils import read_last_scaffolded_project
logger = logging.getLogger(__name__)
WHEEL_DUMP_TARGETS: tuple[str, ...] = ("AZURE", "UC", "S3")

# Refuse to build this distribution — it is the scaffolding CLI, not a user pipeline.
ACC_CLI_DISTRIBUTION_NAME = "acc-cli"

def _normalize_dump_target(raw: str) -> str | None:
    """
    Return the canonical target name, or ``None`` if ``raw`` is not a known target.
    """
    value = raw.strip().upper()
    return value if value in WHEEL_DUMP_TARGETS else None


def _resolve_wheel_dump_target(dump_target: str | None) -> str:
    """
    Resolve the wheel dump target from --dump-target flag or interactive prompt.

    Returns:
        One of WHEEL_DUMP_TARGETS.
    """
    if dump_target is not None:
        normalized = _normalize_dump_target(dump_target)
        if normalized is None:
            allowed = " / ".join(WHEEL_DUMP_TARGETS)
            typer.echo(
                f"Invalid --dump-target '{dump_target}'. Choose one of: {allowed}",
                err=True,
            )
            raise typer.Exit(code=1)
        return normalized

    labels = " / ".join(WHEEL_DUMP_TARGETS)
    answered = typer.prompt(
        f"Where do you want to dump the wheel? [{labels}]",
        default="UC", value_proc=validate_dump_target
    )
    normalized = _normalize_dump_target(answered)
    if normalized is None:
        typer.echo(
            f"Unknown target '{answered}'. Choose from: {list(WHEEL_DUMP_TARGETS)}",
            err=True,
        )
        raise typer.Exit(code=1)
    return normalized

def _find_project_root(start: Path | None = None) -> Path:
    """
    Walk from start (or cwd) upward until a directory containing pyproject.toml.

    Raises:
        typer.Exit: If no pyproject.toml found anywhere in the tree.
    """
    start = start or Path.cwd()
    for directory in (start, *start.parents):
        if (directory / "pyproject.toml").is_file():
            return directory

    typer.echo(
        "No pyproject.toml found in the current directory or any parent directory. "
        "Run this command from inside the project you created with `acc init` "
        "(the folder that contains pyproject.toml).",
        err=True,
    )
    raise typer.Exit(code=1)


def _project_distribution_name(project_root: Path) -> str | None:
    """
    Return normalized [project].name from pyproject.toml, or None if unreadable.
    Used to block acc-cli itself from being built as a target project.
    """
    path = project_root / "pyproject.toml"
    if not path.is_file():
        return None
    try:
        with path.open("rb") as handle:
            data = tomllib.load(handle)
    except (OSError, tomllib.TOMLDecodeError, TypeError, ValueError):
        return None
    project = data.get("project")
    if not isinstance(project, dict):
        return None
    name = project.get("name")
    if not isinstance(name, str):
        return None
    return name.strip().lower()


def _reject_if_tooling_package(project_root: Path) -> None:
    """
    Raises typer.Exit if project_root is the acc-cli package itself.
    Prevents the user from accidentally building the scaffolding tool
    instead of their pipeline project.
    """
    if _project_distribution_name(project_root) == ACC_CLI_DISTRIBUTION_NAME:
        typer.echo(
            "This folder is the acc-cli tool package. acc build only builds the project "
            "you created with acc init — not the generator itself. "
            "Run acc init, cd into that new project, then acc build.",
            err=True,
        )
        raise typer.Exit(code=1)


def _resolve_build_project_root(here: bool) -> Path:
    """
    Resolve the project root for building.

    acc build does NOT require a prior acc init — it will fall back to
    the nearest pyproject.toml in the directory tree automatically.

    Raises:
        typer.Exit: If no pyproject.toml found anywhere.
    """
    if here:
        project_root = _find_project_root().resolve()
        typer.echo(f"Building from current directory: {project_root}")
        _reject_if_tooling_package(project_root)
        return project_root

    # try last scaffolded project first
    last = read_last_scaffolded_project()
    if last is not None:
        typer.echo(f"Building last scaffolded project: {last}")
        _reject_if_tooling_package(last)
        return last

    # fall back to nearest pyproject.toml in cwd tree
    project_root = _find_project_root().resolve()
    typer.echo(f"Building project: {project_root}")
    _reject_if_tooling_package(project_root)
    return project_root


def _pick_latest_wheel(dist_dir: Path) -> Path:
    """
    Return the most recently modified *.whl under dist_dir.

    Raises:
        typer.Exit: If no wheel files exist after build.
    """
    wheels = sorted(dist_dir.glob("*.whl"), key=lambda p: p.stat().st_mtime, reverse=True)
    if not wheels:
        typer.echo(f"No .whl file found in {dist_dir} after build.", err=True)
        raise typer.Exit(code=1)
    return wheels[0]

def _upload_s3(whl_path: Path) -> str:
    """
    Uploads wheel directly from dist/ to AWS S3.

    Returns:
        s3://<bucket>/<folder>/<wheel>.whl
    """
    import boto3
    from botocore.exceptions import ClientError, NoCredentialsError, ParamValidationError, EndpointConnectionError, PartialCredentialsError

    creds  = get_s3_credentials()
    bucket = typer.prompt("S3 Bucket name", value_proc=validate_s3_bucket)
    folder = typer.prompt("S3 Folder", default="wheels",value_proc=validate_s3_folder)
    s3_key = f"{folder.rstrip('/')}/{whl_path.name}"

    client_kwargs: dict = {
        "aws_access_key_id"     : creds["access_key_id"],
        "aws_secret_access_key" : creds["secret_access_key"],
        "region_name"           : creds["region"],
    }
    if "session_token" in creds and creds["session_token"]:
        client_kwargs["aws_session_token"] = creds["session_token"]

    try:
        s3 = boto3.client("s3", **client_kwargs)
        s3.upload_file(str(whl_path), bucket, s3_key)
        remote = f"s3://{bucket}/{s3_key}"
        typer.echo(f"  uploaded → {remote}")
        return remote

    except ClientError as e:
        code = e.response["Error"]["Code"]
        if code == "ExpiredToken":
            invalidate_s3_session_token()
            raise typer.Exit(code=1)  # safety net
        elif code in (
            "InvalidClientTokenId",
            "AccessDenied",
            "AuthFailure",
            "SignatureDoesNotMatch",
        ):
            invalidate_s3()
            raise typer.Exit(code=1)  # safety net
        raise RuntimeError(f"S3 upload failed: {e}")

    except NoCredentialsError:
        invalidate_s3()
        raise typer.Exit(code=1)

    except PartialCredentialsError:
        typer.echo("Incomplete AWS credentials provided.")
        invalidate_s3()
        raise typer.Exit(code=1)

    except ParamValidationError as e:
        raise RuntimeError(f"Invalid S3 parameters: {e}")

    except EndpointConnectionError:
        raise RuntimeError(
            f"Could not connect to S3 endpoint in region '{creds['region']}'."
        )

def _upload_uc_volume(whl_path: Path) -> str:
    """
    Uploads wheel directly from dist/ to a Unity Catalog Volume.
    Returns:
        dbfs:/Volumes/<catalog>/<schema>/<volume>/<wheel>.whl
    """
    from databricks.sdk import WorkspaceClient
    from databricks.sdk.errors import NotFound, PermissionDenied, Unauthenticated

    creds = get_databricks_uc_credentials()

    volume_path = typer.prompt(
        "UC Volume path (format: /Volumes/<catalog>/<schema>/<volume>)",
        default="/Volumes/workspace/default/volume1",value_proc=validate_uc_volume_path,
    )

    if not volume_path.startswith("/Volumes/"):
        typer.echo(
            f"\n[!] Invalid volume path: '{volume_path}'\n"
            f"    Expected format : /Volumes/<catalog>/<schema>/<volume>\n"
            f"    Example         : /Volumes/workspace/default/volume1\n",
            err=True,
        )
        raise typer.Exit(code=1)

    remote_path = f"{volume_path.rstrip('/')}/{whl_path.name}"

    try:
        client = WorkspaceClient(host=creds["host"], token=creds["token"])
        with open(whl_path, "rb") as f:
            client.files.upload(remote_path, f, overwrite=True)
        remote = f"dbfs:{remote_path}"
        typer.echo(f"  uploaded → {remote}")
        return remote

    except (PermissionDenied, Unauthenticated):
        invalidate_databricks_uc()
        raise typer.Exit(code=1)  # safety net — invalidate already raises

    except NotFound:
        typer.echo(
            f"\n[!] Volume not found: {volume_path}\n"
            f"    Make sure the catalog, schema, and volume exist in Databricks.\n",
            err=True,
        )
        raise typer.Exit(code=1)

def _upload_azure(whl_path: Path) -> str:
    """
    Uploads wheel directly from dist/ to Azure ADLS Gen2.
    Uses explicit StorageSharedKeyCredential from ~/.acc/credentials.toml.

    Returns:
        abfss://<container>@<account>.dfs.core.windows.net/<path>/<wheel>.whl
    """
    try:
        from azure.core.exceptions import ClientAuthenticationError, HttpResponseError
    except ImportError:
        raise RuntimeError(
            "Azure SDK not installed.\n"
            "Run: pip install azure-storage-file-datalake azure-storage-blob"
        )
    creds = get_azure_credentials()
    account_name = creds["account_name"]
    account_key = creds["account_key"]

    destination = typer.prompt(
        "Azure destination:\n"
        "  ADLS Gen2 : abfss://<container>@<account>.dfs.core.windows.net/<folder>\n"
        "  Blob      : https://<account>.blob.core.windows.net/<container>/<folder>\n"
        "Destination", value_proc=validate_adls_destination,
    ).strip()

    if destination.startswith("abfss://"):
        # ADLS Gen2 format: abfss://container@account.dfs.core.windows.net/folder
        without_scheme = destination[len("abfss://"):]
        container, rest = without_scheme.split("@", 1)
        account_host, remote_dir = rest.split("/", 1)
        remote_file_path = f"{remote_dir.strip('/')}/{whl_path.name}"
        use_datalake = True

    elif destination.startswith("https://") and ".blob.core.windows.net" in destination:
        # Blob format: https://account.blob.core.windows.net/container/folder
        without_scheme = destination[len("https://"):]
        account_host, rest = without_scheme.split("/", 1)
        container, remote_dir = rest.split("/", 1)
        remote_file_path = f"{remote_dir.strip('/')}/{whl_path.name}"
        use_datalake = False

    else:
        typer.echo(
            "\n[!] Unrecognised destination format.\n"
            "    ADLS Gen2 : abfss://<container>@<account>.dfs.core.windows.net/<folder>\n"
            "    Blob      : https://<account>.blob.core.windows.net/<container>/<folder>\n",
            err=True,
        )
        raise typer.Exit(code=1)
    # REPLACE with this:
    try:
        if use_datalake:
            from azure.storage.filedatalake import DataLakeServiceClient
            service = DataLakeServiceClient(
                account_url=f"https://{account_host}",
                credential={"account_name": account_name, "account_key": account_key},
            )
            file_client = (
                service
                .get_file_system_client(container)
                .get_file_client(remote_file_path)
            )
            with open(whl_path, "rb") as f:
                file_client.upload_data(f.read(), overwrite=True)
            remote = f"abfss://{container}@{account_host}/{remote_file_path}"

        else:
            from azure.storage.blob import BlobServiceClient
            service = BlobServiceClient(
                account_url=f"https://{account_host}",
                credential={"account_name": account_name, "account_key": account_key},
            )
            blob_client = service.get_blob_client(container=container, blob=remote_file_path)
            with open(whl_path, "rb") as f:
                blob_client.upload_blob(f, overwrite=True)
            remote = f"https://{account_host}/{container}/{remote_file_path}"

        typer.echo(f"  uploaded → {remote}")
        return remote

    except ClientAuthenticationError:
        invalidate_azure()
        raise typer.Exit(code=1)

    except HttpResponseError as e:
        if e.status_code == 403:
            invalidate_azure()
            raise typer.Exit(code=1)
        raise RuntimeError(f"Azure upload failed: {e}")


def _upload(whl_path: Path, target: str) -> str:
    """
    Routes to correct upload backend based on dump target.
    whl_path always points to the file inside dist/.

    Returns:
        Remote URI written to manifest.
    """
    router = {
        "S3"    : _upload_s3,
        "UC"    : _upload_uc_volume,
        "AZURE" : _upload_azure,
    }
    return router[target](whl_path)

def build_command(
    here: bool = typer.Option(
        False,
        "--here",
        help="Build from the current directory tree instead of the last scaffolded project.",
    ),
    dump_target: str | None = typer.Option(
        None,
        "--dump-target",
        help="Where to upload the wheel (AZURE / UC / S3). "
             "Prompted interactively if omitted.",
    ),
) -> None:
    """
    Build a wheel and upload it to the chosen destination.

    Credentials stored in ~/.acc/credentials.toml after the first run.
    [s3], [databricks_uc] are stored separately.
    Writes acc-build-manifest.json with wheel_path, target, and remote_path.
    """
    try:
        logger.info("Starting acc build")

        # RESOLVE PROJECT ROOT
        project_root = _resolve_build_project_root(here)

        #RESOLVE DUMP TARGET
        wheel_dump_target = _resolve_wheel_dump_target(dump_target)
        typer.echo(f"Wheel dump target: {wheel_dump_target}")

        #BUILD WHEEL
        logger.info("Building wheel from %s", project_root)
        completed = subprocess.run(
            [sys.executable, "-m", "build", "--wheel"],
            cwd=project_root,
            check=False,
        )
        if completed.returncode != 0:
            typer.echo("Wheel build failed. Fix errors above and retry.", err=True)
            raise typer.Exit(code=completed.returncode)

        dist_dir = project_root / "dist"
        if not dist_dir.is_dir():
            typer.echo(f"Expected dist directory missing: {dist_dir}", err=True)
            raise typer.Exit(code=1)

        whl_path = _pick_latest_wheel(dist_dir)
        typer.echo(f"  built   → {whl_path.name} (in dist/)")

        #UPLOAD
        typer.echo(f"\nUploading to {wheel_dump_target}...")
        remote_location = _upload(whl_path, wheel_dump_target)

        #WRITE MANIFEST
        manifest_path = project_root / "acc-build-manifest.json"
        manifest_path.write_text(
            json.dumps(
                {
                    "wheel_path"         : str(whl_path),
                    "built_from_project" : str(project_root),
                    "wheel_dump_target"  : wheel_dump_target,
                    "remote_path"        : remote_location,
                },
                indent=2,
            ),
            encoding="utf-8",
        )

        typer.echo("\nBuild finished successfully.")
        typer.echo(f"Wheel    : {whl_path}")
        typer.echo(f"Remote   : {remote_location}")
        typer.echo(f"Manifest : {manifest_path}")

        logger.info("acc build completed: %s → %s", whl_path, remote_location)

    except typer.Exit:
        raise
    except Exception as exc:
        logger.exception("acc build failed")
        typer.echo(f"Error: {exc}", err=True)
        raise typer.Exit(code=1)