"""``acc build`` — produce a wheel, copy it beside ``pyproject.toml``, and write a manifest.

Build only runs for the project last created with ``acc`` / ``acc init`` (stored under
``~/.acc``). The ACC CLI package itself (``acc-cli``) cannot be built as the target.

The dump target (AZURE / DBFS / UC / S3) records *intent* for downstream deploy steps;
it does not upload the wheel by itself.
"""

from __future__ import annotations

import json
import logging
import shutil
import subprocess
import sys
import tomllib
from pathlib import Path
from acc_cli.utils.azure import upload_to_azure
from acc_cli.utils.dbfs import upload_to_dbfs

import typer

from acc_cli.utils.utils import read_last_scaffolded_project

logger = logging.getLogger(__name__)

# Where the user plans to use the wheel next (stored in acc-build-manifest.json).
WHEEL_DUMP_TARGETS: tuple[str, ...] = ("AZURE", "DBFS", "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. e.g " dbfs " → "DBFS"""
    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`` or an interactive prompt.

    Args:
        dump_target: CLI value when ``--dump-target`` was passed; ``None`` means prompt.

    Returns:
        One of ``WHEEL_DUMP_TARGETS``.

    Raises:
        typer.Exit: On invalid CLI value or invalid answer at the prompt.
    """
    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}", #e.g "Invalid --dump-target ' dbfs '. Choose one of: AZURE / DBFS / UC / S3"
                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="DBFS",
    )
    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``.

    Returns:
        The directory that holds the project root marker.

    Raises:
        typer.Exit: If no ``pyproject.toml`` is found in the walk.
    """
    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` "
        "(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``."""
    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:
    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:
    """
    Require a prior scaffold (``~/.acc/last_project.json``).

    With ``--here``, cwd must lie inside that same project (resolved root must match).
    """
    last = read_last_scaffolded_project()
    if last is None:
        typer.echo(
            "No acc-scaffolded project on file. Run `acc init` (or `acc` with no "
            "subcommand) to create a project first, then run `acc build`.",
            err=True,
        )
        raise typer.Exit(code=1)

    last_resolved = last.resolve()

    if here:
        discovered = _find_project_root().resolve()
        if discovered != last_resolved:
            typer.echo(
                "acc build --here only works inside the same project you last created "
                f"with acc init:\n  expected: {last_resolved}\n  found:    {discovered}",
                err=True,
            )
            raise typer.Exit(code=1)
        project_root = discovered
    else:
        project_root = last_resolved

    _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.
    """
    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 build_command(
    here: bool = typer.Option(
        False,
        "--here",
        help="Resolve the project root from cwd; it must be the same folder as the "
        "last project created with acc init.",
    ),
    dump_target: str | None = typer.Option(
        None,
        "--dump-target",
        help="Where the wheel will be used (AZURE, DBFS, UC, S3). "
        "If omitted, you are prompted interactively.",
    ),
) -> None:
    """
    Build a wheel only for the project last created with ``acc init`` (see ``~/.acc``).
    Fails if there is no such project on file.
    Use ``--here`` when your cwd is inside that project (root must match the saved path).
    Writes ``acc-build-manifest.json``.
    """
    try:
        logger.info("Starting acc build")

        # Require a scaffolded project before any interactive dump-target prompt.
        project_root = _resolve_build_project_root(here)
        if here:
            typer.echo(f"Building scaffolded project (from cwd): {project_root}")
        else:
            typer.echo(f"Building last scaffolded project: {project_root}")

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

        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)

        built_wheel = _pick_latest_wheel(dist_dir)
        final_path = project_root / built_wheel.name

        # Keep a copy at project root (easy to find; dist/ may contain older wheels).
        shutil.copy2(built_wheel, final_path)

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

        typer.echo("\nBuild finished successfully.")
        typer.echo(f"Wheel file: {final_path}")
        typer.echo(f"Manifest:   {manifest_path}")

        logger.info("acc build completed: %s", final_path)

    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)
