import json
import logging
from pathlib import Path
import functools
import typer
import yaml
from cookiecutter.main import cookiecutter

from acc_cli.config import TEMPLATES, TEMPLATES_DIR
from acc_cli.utils.validators import (
    validate_project_name,
    validate_version,
    validate_author,
    validate_output_dir,
)

logger = logging.getLogger(__name__)
# INPUT
def collect_inputs():
    """
    Collects user input from the CLI to scaffold a new project.

    This function:
    - Displays available project templates
    - Prompts the user for project metadata (name, version, author)
    - Validates selected template
    - Collects output directory path
    - Allows user to exit at any prompt by typing 'q'

    Returns:
        dict: A dictionary containing all user inputs required for project generation.
    """
    def prompt_or_exit(prompt_text, default=None, value_proc=None):
        """Helper to add exit option to every prompt."""
        hint = f" [or 'q' to quit]"
        full_prompt = prompt_text + hint

        while True:
            if default is not None:
                value = typer.prompt(full_prompt, default=default)
            else:
                value = typer.prompt(full_prompt)

            if str(value).strip().lower() == "q":
                typer.echo("\nExiting scaffolder. Goodbye!")
                raise typer.Exit(code=0)

            if value_proc:
                try:
                    return value_proc(value)
                except Exception as e:
                    typer.echo(f"  ✗ {e}", err=True)
            else:
                return value

    def prompt_numbered_choice(label, choices, default_index=0):
        """
        Display a numbered menu and return the chosen item.
        User enters a number; 'q' exits.
        """
        typer.echo(f"\n{label}:")
        for i, choice in enumerate(choices, start=1):
            marker = " (default)" if i - 1 == default_index else ""
            typer.echo(f"  {i}. {choice}{marker}")

        while True:
            raw = typer.prompt(f"Enter number [1-{len(choices)}] or 'q' to quit",
                               default=str(default_index + 1))

            if str(raw).strip().lower() == "q":
                typer.echo("\nExiting scaffolder. Goodbye!")
                raise typer.Exit(code=0)

            try:
                index = int(raw) - 1
                if 0 <= index < len(choices):
                    chosen = choices[index]
                    typer.echo(f" Selected: {chosen}")
                    return chosen
                else:
                    typer.echo(f"  ✗ Please enter a number between 1 and {len(choices)}.")
            except ValueError:
                typer.echo("  ✗ Invalid input. Enter a number or 'q' to quit.")
    typer.echo("\nACC Project Scaffolder\n")
    typer.echo("(Type 'q' at any prompt to exit)\n")

    #Text inputs with exit option
    project_name = prompt_or_exit("Project name", value_proc=validate_project_name)
    version      = prompt_or_exit("Version", default="0.1.0", value_proc=validate_version)
    author       = prompt_or_exit("Author", value_proc=validate_author)

    #Template: numbered selection
    template_keys  = list(TEMPLATES.keys())
    template_descs = [f"{k} — {TEMPLATES[k]['description']}" for k in template_keys]

    typer.echo("\nAvailable templates:")
    chosen_desc = prompt_numbered_choice("Select a template", template_descs, default_index=0)
    # Map description back to key
    template = template_keys[template_descs.index(chosen_desc)]
    etl_type  = None
    etl_types = TEMPLATES[template].get("etl_types", [])
    if etl_types:
        etl_type = prompt_numbered_choice("Select ETL type", etl_types, default_index=0)
    ml_type  = None
    ml_types = TEMPLATES[template].get("ml_types", [])
    if ml_types:
        ml_type = prompt_numbered_choice("Select ML type", ml_types, default_index=0)
    output_dir = prompt_or_exit(
        "Output directory",
        default=str(Path.cwd()),
        value_proc=validate_output_dir,
    )

    return {
        "project_name": project_name,
        "version":      version,
        "author":       author,
        "template":     template,
        "etl_type":     etl_type,
        "ml_type":      ml_type,
        "output_dir":   output_dir,
    }
# HELPERS
def generate_slug(name: str) -> str:
    """
    Converts a project name into a filesystem-safe slug.

    Replaces spaces and hyphens with underscores and converts to lowercase.

    Args:
        name (str): Original project name.

    Returns:
        str: Normalized slug suitable for directories or package names.
    """
    return name.replace("-", "").replace(" ", "").lower()

def load_structure(template: str) -> dict:
    yml_dir = TEMPLATES[template].get("yml_dir", TEMPLATES[template]["dir"])
    yml_path = TEMPLATES_DIR / yml_dir / "structure.yml"
    with open(yml_path, encoding="utf-8") as f:
        return yaml.safe_load(f)
def load_modules(template: str) -> dict:
    """
    Loads all module YMLs for a template into a flat dict.
    e.g. { "standard": {"extract": "...", "transform": "..."}, "medallion": {...} }
    """
    yml_dir = TEMPLATES[template].get("yml_dir", TEMPLATES[template]["dir"])
    modules_dir = TEMPLATES_DIR / yml_dir / "functions"
    modules = {}
    for yml_file in modules_dir.glob("*.yml"):
        with open(yml_file, encoding="utf-8") as f:
            modules[yml_file.stem] = yaml.safe_load(f)
    return modules
def resolve_path(raw: str, project_slug: str) -> str:
    return raw.replace("{{project_slug}}", project_slug)
# COOKIECUTTER EXECUTION
def generate_project(template_dir: str, output_dir: str, context: dict):
    """
    Generates a project scaffold using a Cookiecutter template.

    Args:
        template_dir (str): Path to the Cookiecutter template directory.
        output_dir (str): Directory where the project will be generated.
        context (dict): Template variables used to render the project.

    Raises:
        RuntimeError: If Cookiecutter execution fails.
    """
    try:
        cookiecutter(
            template_dir,
            no_input=True,
            output_dir=output_dir,
            extra_context=context,
        )
    except Exception as e:
        raise RuntimeError(f"Cookiecutter failed: {e}")


def resolve_content(entry: dict, modules: dict, template: str) -> str | None:
    """
    Resolve file body for a structure.yml entry.

    - No ``source``: use ``entry["content"]`` (or empty string).
    - Path with ``/`` (e.g. ``readme/standard.md`` or
      ``{{cookiecutter.project_slug}}/Makefile``): file under the template dir.
    - ``module.key``: string from ``functions/<module>.yml``.

    Missing files: returns ``None`` (caller skips the write).
    Missing module key: returns empty string.
    """
    source = entry.get("source")
    if not source:
        return entry.get("content", "")

    template_root = TEMPLATES_DIR / TEMPLATES[template]["dir"]

    if "/" in source:
        path = template_root / source
        if path.is_file():
            return path.read_text(encoding="utf-8")
        logger.warning("Template file missing: %s", path)
        return None

    module_name, key = source.split(".", 1)
    return modules.get(module_name, {}).get(key, "")


def generate_project_from_yml(
    template: str,
    etl_type: str | None,
    output_dir: str,
    project_slug: str,
):
    structure = load_structure(template)
    modules = load_modules(template)
    base = Path(output_dir)

    all_files = structure.get("shared", [])

    if etl_type and "etl_types" in structure:
        matched = structure["etl_types"].get(etl_type, [])
        all_files += matched
    for entry in all_files:
        if isinstance(entry, str):
            raw_path = entry
            content = ""
        else:
            raw_path = entry["path"]
            content = resolve_content(entry, modules, template)

        full_path = base / resolve_path(raw_path, project_slug)
        full_path.parent.mkdir(parents=True, exist_ok=True)
        if content is None:
            typer.echo(f"  skipped (missing source file) -> {full_path}")
            continue
        full_path.write_text(content, encoding="utf-8")
        typer.echo(f"  created -> {full_path}")
# OUTPUT
def print_summary(project_path: Path, name: str, version: str):
    """
    Prints a summary of the generated project and next steps.

    Args:
        project_path (Path): Path where the project was created.
        name (str): Project name.
        version (str): Project version.
    """
    typer.echo("\nProject created successfully!\n")
    typer.echo(f"Location:   {project_path.resolve()}")
    typer.echo("\nNext steps:")
    typer.echo("  acc build")
    typer.echo("  make run")
    typer.echo("  make test")
    typer.echo("  make lint")
def write_last_scaffolded_project(project_root: Path) -> None:
    """Persist the last project path under ``~/.acc`` so ``acc build`` can resolve it."""
    root = project_root.expanduser().resolve()
    state_dir = Path.home() / ".acc"
    state_file = state_dir / "last_project.json"
    state_dir.mkdir(parents=True, exist_ok=True)
    state_file.write_text(
        json.dumps({"project_root": str(root)}, indent=2),
        encoding="utf-8",
    )
    logger.info("Recorded last scaffolded project: %s", root)


def read_last_scaffolded_project() -> Path | None:
    """Load the last scaffolded project path if present and still valid."""
    state_file = Path.home() / ".acc" / "last_project.json"
    if not state_file.is_file():
        return None
    try:
        data = json.loads(state_file.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError) as exc:
        logger.warning("Could not read %s: %s", state_file, exc)
        return None
    raw = data.get("project_root")
    if not raw or not isinstance(raw, str):
        return None
    path = Path(raw).expanduser().resolve()
    if not path.is_dir():
        logger.warning("Last scaffolded path missing or not a directory: %s", path)
        return None
    if not (path / "pyproject.toml").is_file():
        logger.warning("Last scaffolded path has no pyproject.toml: %s", path)
        return None
    return path


