"""Scaffold rendering engine for exokit.

Orchestrates the full project generation:
1. Create directory structure (lakehouse stubs, scripts, docs).
2. Render all .j2 templates via Jinja2 into the output directory.
3. Build the demo manifest and write it to conf/.
4. Return a ``ScaffoldResult`` with every file created.
"""

from __future__ import annotations

import json
import re
from pathlib import Path

from jinja2 import Environment, FileSystemLoader, StrictUndefined

from exokit.core.manifest import build_manifest
from exokit.core.models import ScaffoldContext, ScaffoldResult

# ---------------------------------------------------------------------------
# Custom Jinja2 filters
# ---------------------------------------------------------------------------

_NON_ALPHANUM = re.compile(r"[^a-z0-9]+")
_WORD_BOUNDARY = re.compile(r"[\s\-_]+")

INDUSTRY_DISPLAY_NAMES: dict[str, str] = {
    "fsi": "Financial Services",
    "telecom": "Telecommunications",
    "retail": "Retail",
    "healthcare": "Healthcare",
    "manufacturing": "Manufacturing",
}


def _slugify(value: str) -> str:
    """Convert a string to a URL/file-safe slug.

    >>> _slugify("Acme Bank FSI")
    'acme-bank-fsi'
    """
    return _NON_ALPHANUM.sub("-", value.lower()).strip("-")


def _snake_case(value: str) -> str:
    """Convert a string to snake_case.

    >>> _snake_case("Acme Bank FSI")
    'acme_bank_fsi'
    """
    return re.sub(r"[^a-z0-9]+", "_", value.lower()).strip("_")


def _title_case(value: str) -> str:
    """Convert a string to Title Case, splitting on common delimiters.

    >>> _title_case("acme-bank-fsi")
    'Acme Bank Fsi'
    """
    return " ".join(word.capitalize() for word in _WORD_BOUNDARY.split(value))


# ---------------------------------------------------------------------------
# Jinja2 environment builder
# ---------------------------------------------------------------------------


def _build_jinja_env(templates_dir: Path) -> Environment:
    """Create a configured Jinja2 ``Environment``.

    Args:
        templates_dir: Root directory containing ``.j2`` template files.

    Returns:
        A ready-to-use Jinja2 environment with custom filters and strict mode.
    """
    env = Environment(  # noqa: S701 — renders .py/.yml/.sh, not HTML
        loader=FileSystemLoader(str(templates_dir)),
        undefined=StrictUndefined,
        trim_blocks=True,
        lstrip_blocks=True,
        keep_trailing_newline=True,
    )
    env.filters["slugify"] = _slugify
    env.filters["snake_case"] = _snake_case
    env.filters["title_case"] = _title_case
    return env


# ---------------------------------------------------------------------------
# Directory structure
# ---------------------------------------------------------------------------

# Directories to create inside the generated project (lakehouse stubs, etc.)
_STUB_DIRECTORIES: list[str] = [
    "conf",
    "docs",
    "docs/supplements",
    "scripts",
    "src/data_generation/structured",
    "src/data_generation/unstructured",
    "src/pipelines/bronze",
    "src/pipelines/silver",
    "src/pipelines/gold",
    "src/ml",
    "src/dashboards",
    "src/agents",
    "resources/jobs",
    "resources/pipelines",
    "resources/dashboards",
    ".claude/agents",
    ".claude/commands",
    ".claude/skills",
    ".claude/agent-memory/lakehouse-dev",
    ".claude/agent-memory/app-dev",
    ".claude/agent-memory/dashboard-dev",
    ".claude/agent-memory/reviewer",
]


def _create_directories(output_dir: Path) -> list[str]:
    """Create all stub directories in *output_dir*.

    Returns:
        List of directory paths created (relative to *output_dir*).
    """
    created: list[str] = []
    for d in _STUB_DIRECTORIES:
        target = output_dir / d
        target.mkdir(parents=True, exist_ok=True)
        created.append(d)
    return created


def _render_templates(
    env: Environment,
    template_context: dict[str, object],
    output_dir: Path,
) -> list[str]:
    """Render every ``.j2`` template found in the Jinja2 environment.

    Subdirectory structure under the templates root is preserved.  The ``.j2``
    extension is stripped from the output filename.

    Args:
        env: Configured Jinja2 environment.
        template_context: Variables available inside templates.
        output_dir: Root of the generated project.

    Returns:
        Sorted list of rendered file paths (relative to *output_dir*).
    """
    files_created: list[str] = []
    template_names = sorted(env.loader.list_templates())  # type: ignore[union-attr]

    for tpl_name in template_names:
        # Skip non-j2 files (static assets, etc.)
        if not tpl_name.endswith(".j2"):
            continue

        # Determine output path: strip .j2, map template subdirs to project dirs
        out_rel = _map_template_path(tpl_name)
        out_path = output_dir / out_rel
        out_path.parent.mkdir(parents=True, exist_ok=True)

        template = env.get_template(tpl_name)
        rendered = template.render(template_context)
        out_path.write_text(rendered, encoding="utf-8")
        files_created.append(out_rel)

    return sorted(files_created)


def _map_template_path(template_name: str) -> str:
    """Map a template-relative path to a project-relative output path.

    The ``.j2`` extension is stripped.  Template subdirectories map to project
    directories as follows:

    - ``root/*``      -> project root
    - ``conf/*``      -> ``conf/``
    - ``claude/*``    -> ``.claude/``
    - ``docs/*``      -> ``docs/``
    - ``lakehouse/*`` -> (various ``src/`` and ``resources/`` paths)
    - ``scripts/*``   -> ``scripts/``

    Anything else is placed at the project root with ``.j2`` stripped.
    """
    # Strip .j2 extension
    out = template_name.removesuffix(".j2")

    # Map top-level template directories to project paths
    if out.startswith("root/"):
        return out.removeprefix("root/")
    if out.startswith("claude/"):
        return f".claude/{out.removeprefix('claude/')}"
    if out.startswith("lakehouse/"):
        return out.removeprefix("lakehouse/")
    # conf/, docs/, scripts/ pass through as-is
    return out


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

_DEFAULT_TEMPLATES_DIR = Path(__file__).parent / "templates"


def generate(
    context: ScaffoldContext,
    output_dir: Path,
    *,
    templates_dir: Path | None = None,
) -> ScaffoldResult:
    """Orchestrate the full project scaffold.

    Renders templates and writes the manifest.
    Does NOT call apx_bridge or skills — those are called by the CLI
    separately so they can be individually skipped/retried.

    Args:
        context: Fully-resolved scaffold context (answers + computed fields).
        output_dir: Root directory for the generated project.
        templates_dir: Override the templates directory (useful for testing).

    Returns:
        A ``ScaffoldResult`` summarising the generated project.
    """
    templates_dir = templates_dir or _DEFAULT_TEMPLATES_DIR
    files_created: list[str] = []
    warnings: list[str] = []

    # 1. Create directory structure
    output_dir.mkdir(parents=True, exist_ok=True)
    _create_directories(output_dir)

    # Add .gitkeep to agent-memory dirs (Git doesn't track empty dirs)
    for agent_dir in [
        "lakehouse-dev",
        "app-dev",
        "dashboard-dev",
        "reviewer",
    ]:
        gitkeep = output_dir / ".claude" / "agent-memory" / agent_dir / ".gitkeep"
        gitkeep.parent.mkdir(parents=True, exist_ok=True)
        if not gitkeep.exists():
            gitkeep.write_text("", encoding="utf-8")

    # 2. Build Jinja2 context from the Pydantic model
    template_context: dict[str, object] = context.model_dump()
    # Add display_name: company if set, otherwise human-readable industry name
    company_str = str(template_context.get("company", ""))
    industry_str = str(template_context.get("industry", ""))
    template_context["display_name"] = (
        company_str
        if company_str
        else INDUSTRY_DISPLAY_NAMES.get(industry_str, industry_str.upper())
    )

    # Build demo title: avoids redundancy like "Financial Services FSI Demo"
    if company_str:
        template_context["demo_title"] = f"{company_str} {industry_str.upper()} Demo"
    else:
        template_context["demo_title"] = (
            f"{INDUSTRY_DISPLAY_NAMES.get(industry_str, industry_str.upper())} Demo"
        )

    # 3. Render all .j2 templates
    env = _build_jinja_env(templates_dir)
    rendered = _render_templates(env, template_context, output_dir)
    files_created.extend(rendered)

    # 4. Build manifest and write to conf/ (overwrites template-rendered version)
    manifest = build_manifest(context)
    manifest_path = output_dir / "conf" / "demo_manifest.json"
    manifest_path.parent.mkdir(parents=True, exist_ok=True)
    manifest_path.write_text(
        json.dumps(manifest.model_dump(), indent=2, default=str) + "\n",
        encoding="utf-8",
    )
    # Only add to files_created if the template didn't already produce it
    if "conf/demo_manifest.json" not in files_created:
        files_created.append("conf/demo_manifest.json")

    # 5. Warn if no templates were found (template-dev may still be working)
    if not rendered:
        warnings.append(
            "No .j2 templates found in templates directory. "
            "Templates may be created by the template-dev agent in parallel."
        )

    return ScaffoldResult(
        project_dir=output_dir,
        files_created=sorted(files_created),
        warnings=warnings,
        manifest=manifest,
    )
