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

logger = logging.getLogger(__name__)
from acc_cli.utils.validators import (
    validate_project_name,
    validate_version,
    validate_author,
    validate_template,
    validate_etl_type,
    validate_output_dir,
)
# 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

    Returns:
        dict: A dictionary containing all user inputs required for project generation.
    """
    typer.echo("\nACC Project Scaffolder\n")

    typer.echo("Available templates:")
    for key, meta in TEMPLATES.items():
        typer.echo(f"  {key:<10} — {meta['description']}")
    typer.echo()

    project_name = typer.prompt("Project name",value_proc=validate_project_name)
    version = typer.prompt("Version", default="0.1.0", value_proc=validate_version)
    author = typer.prompt("Author", value_proc=validate_author)

    template = typer.prompt(
        f"Template [{' / '.join(TEMPLATES.keys())}]",
        default="etl",
        value_proc = validate_template
    )

    if template not in TEMPLATES:
        typer.echo(
            f"Unknown template '{template}'. Choose from: {list(TEMPLATES.keys())}",
            err=True
        )
        raise typer.Exit(code=1)
    etl_type = None
    etl_types = TEMPLATES[template].get("etl_types", [])
    if etl_types:
        etl_type = typer.prompt(
            f"ETL type [{' / '.join(etl_types)}]",
            default=etl_types[0],
        value_proc=functools.partial(validate_etl_type, template=template)
        )
    ml_type = None
    ml_types = TEMPLATES[template].get("ml_types", [])
    if ml_types:
        ml_type = typer.prompt(
            f"ML type [{' / '.join(ml_types)}]",
            default=ml_types[0],
        )
    output_dir = typer.prompt(
        "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:
    """
    Resolves and returns the content for a file entry defined in structure.yml.

    This function determines how file content should be populated based on the
    `entry` configuration. Content can come from one of three sources:

    1. Inline content:
       If no `source` field is provided, returns `entry["content"]` if present.

    2. File-based content:
       If `source` starts with "readme/", it is treated as a relative file path
       inside the template directory. The file is read and its contents returned.

    3. Module-based content:
       Otherwise, `source` is expected in the format "module.key".
       The function looks up the corresponding value from loaded module YAMLs
       (e.g., functions/<module>.yml).

    Args:
        entry (dict): File definition from structure.yml. May contain:
                      - "content" (inline text)
                      - "source" (external reference)
        modules (dict): Loaded module definitions from functions/*.yml
        template (str): Template name used to resolve file paths

    Returns:
        str: Final resolved content to be written to the file.
             Returns an empty string if no content is found.
    """
    source = entry.get("source")
    if not source:
        return entry.get("content", "")

    # if source points to a .md file in readme/
    if source.startswith("readme/"):
        readme_path = TEMPLATES_DIR / TEMPLATES[template]["dir"] /source
        if readme_path.exists():
            return readme_path.read_text(encoding="utf-8")
        return ""

    # existing yml key lookup
    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)
        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("make build")


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


