"""APX CLI integration bridge for exokit.

Calls ``apx init`` via subprocess to scaffold a Databricks App, then applies
a governance overlay (3-layer directory structure + app-specific CLAUDE.md).
String constants live in ``apx_content``.
"""

from __future__ import annotations

import logging
import subprocess
from pathlib import Path

import yaml

from exokit.core.apx_content import APP_CLAUDE_MD, write_app_readme
from exokit.core.apx_hello_world import write_hello_world_app
from exokit.core.models import ScaffoldContext

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Governance overlay — 3-layer backend directories
# ---------------------------------------------------------------------------

_BACKEND_LAYERS: list[str] = [
    "api",
    "services",
    "repositories",
    "schemas",
    # NOTE: "models" is NOT included — APX creates a models.py file that
    # router.py imports from. A models/ directory would shadow it.
]


# ---------------------------------------------------------------------------
# Errors
# ---------------------------------------------------------------------------


class APXNotInstalledError(Exception):
    """Raised when the APX CLI binary cannot be found on PATH."""


class APXScaffoldError(Exception):
    """Raised when ``apx init`` exits with a non-zero return code."""


class APXTimeoutError(Exception):
    """Raised when ``apx init`` exceeds the timeout."""


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


def scaffold_app(context: ScaffoldContext, app_dir: Path) -> None:
    """Scaffold a Databricks App via ``apx init`` and apply governance overlay.

    Args:
        context: Scaffold context containing app_name and optional databricks_profile.
        app_dir: Directory where the app should be created.

    Raises:
        APXNotInstalledError: If the ``apx`` binary is not on PATH.
        APXScaffoldError: If ``apx init`` exits non-zero.
        APXTimeoutError: If ``apx init`` exceeds 120 seconds.
    """
    _run_apx_init(context, app_dir)

    # Remove APX's nested .git — the root project owns git
    apx_git = app_dir / ".git"
    if apx_git.exists():
        import shutil

        shutil.rmtree(apx_git)
        logger.info("Removed APX's nested .git from %s", app_dir)

    # Post-process APX's generated files
    _postprocess_apx_config(context, app_dir)
    _postprocess_app_env(context, app_dir)
    _install_shadcn_components(context, app_dir)

    # Apply our governance overlay
    _apply_governance_overlay(context, app_dir)

    # Write hello-world app code (sample API + React page)
    write_hello_world_app(context, app_dir)

    # Write enhanced README
    write_app_readme(context, app_dir)


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------


def _run_apx_init(context: ScaffoldContext, app_dir: Path) -> None:
    """Execute ``apx init`` with --name, --addons, optional --profile, and path."""
    cmd: list[str] = [
        "apx",
        "init",
        "--name",
        context.app_name,
        "--addons",
        "ui,sidebar,sql,claude",
    ]

    if context.databricks_profile:
        cmd.extend(["--profile", context.databricks_profile])

    cmd.append(str(app_dir))

    logger.info("Running APX scaffold: %s", " ".join(cmd))

    try:
        result = subprocess.run(  # noqa: S603
            cmd,
            capture_output=True,
            text=True,
            timeout=120,
        )
    except FileNotFoundError as exc:
        msg = "APX CLI not found. Install it with: pip install databricks-apx-cli"
        raise APXNotInstalledError(msg) from exc
    except subprocess.TimeoutExpired as exc:
        msg = "APX scaffold timed out after 120 seconds"
        raise APXTimeoutError(msg) from exc

    if result.returncode != 0:
        msg = f"APX scaffold failed (exit {result.returncode}): {result.stderr.strip()}"
        raise APXScaffoldError(msg)

    logger.info("APX scaffold completed: %s", result.stdout.strip())


def _apply_governance_overlay(context: ScaffoldContext, app_dir: Path) -> None:
    """Apply the 3-layer governance overlay on top of the APX-scaffolded app.

    APX creates its backend at ``src/{app_slug}/backend/``. We add our
    3-layer directories (api/, services/, repositories/, schemas/) inside
    that backend directory, coexisting with APX's core/, router.py, models.py.
    """
    # Derive the app slug (APX convention: hyphens -> underscores)
    app_slug = context.app_name.replace("-", "_")

    # Find APX's backend directory — fail fast if missing
    apx_backend = app_dir / "src" / app_slug / "backend"
    if not apx_backend.exists():
        msg = f"APX did not create expected backend directory at {apx_backend}"
        raise APXScaffoldError(msg)

    # Create 3-layer directories inside APX's backend
    for layer in _BACKEND_LAYERS:
        layer_dir = apx_backend / layer
        layer_dir.mkdir(parents=True, exist_ok=True)
        init_file = layer_dir / "__init__.py"
        if not init_file.exists():
            init_file.write_text(
                f'"""Package: {layer}."""\n',
                encoding="utf-8",
            )
    logger.info("Created 3-layer directories in %s", apx_backend)

    # Write app-specific CLAUDE.md
    claude_md_path = app_dir / "CLAUDE.md"
    claude_md_path.write_text(
        APP_CLAUDE_MD.format(app_name=context.app_name, app_slug=app_slug),
        encoding="utf-8",
    )
    logger.info("Wrote app CLAUDE.md to %s", claude_md_path)


def _postprocess_apx_config(context: ScaffoldContext, app_dir: Path) -> None:
    """Post-process APX's generated databricks.yml to align with project config."""
    config_path = app_dir / "databricks.yml"
    if not config_path.exists():
        logger.warning("No databricks.yml found in %s", app_dir)
        return

    config = yaml.safe_load(config_path.read_text(encoding="utf-8"))
    if not isinstance(config, dict):
        return

    # Remove unwanted database_instances (provisioned Lakebase DB)
    resources: object = config.get("resources", {})
    if isinstance(resources, dict) and "database_instances" in resources:
        del resources["database_instances"]
        logger.info("Removed database_instances from app databricks.yml")

    # Align workspace config with project settings
    targets: object = config.get("targets", {})
    if isinstance(targets, dict):
        for target_config in targets.values():
            if not isinstance(target_config, dict):
                continue
            # Create workspace block if it doesn't exist
            if "workspace" not in target_config:
                target_config["workspace"] = {}
            workspace: object = target_config.get("workspace", {})
            if isinstance(workspace, dict):
                if context.databricks_profile:
                    workspace["profile"] = context.databricks_profile
                if context.workspace_host:
                    workspace["host"] = context.workspace_host

    # Clean up app resources: remove db references, ensure sql_warehouse
    if isinstance(resources, dict):
        apps: object = resources.get("apps", {})
        if isinstance(apps, dict):
            for app_config in apps.values():
                if not isinstance(app_config, dict):
                    continue
                app_resources: object = app_config.get("resources", [])
                if isinstance(app_resources, list):
                    # Remove db entries that reference database_instances
                    filtered: list[object] = [
                        res
                        for res in app_resources
                        if not (isinstance(res, dict) and "database" in res)
                    ]
                    app_config["resources"] = filtered

                    # Ensure sql_warehouse resource exists
                    has_warehouse = False
                    for res in filtered:
                        if isinstance(res, dict) and res.get("name") == "sql-warehouse":
                            res["sql_warehouse"] = {
                                "id": context.warehouse_id,
                                "permission": "CAN_USE",
                            }
                            has_warehouse = True
                    if not has_warehouse:
                        filtered.append(
                            {
                                "name": "sql-warehouse",
                                "sql_warehouse": {
                                    "id": context.warehouse_id,
                                    "permission": "CAN_USE",
                                },
                            }
                        )

    config_path.write_text(
        yaml.dump(config, default_flow_style=False, sort_keys=False),
        encoding="utf-8",
    )
    logger.info("Post-processed app databricks.yml")

    # When database_instances is removed, also disable the Lakebase dependency
    # in the app code — otherwise the app crashes on startup trying to connect.
    _disable_lakebase_dependency(context, app_dir)

    # Ensure app.yml has the warehouse env var for SqlConfig
    _postprocess_app_yml(context, app_dir)


def _postprocess_app_yml(context: ScaffoldContext, app_dir: Path) -> None:
    """Ensure app.yml passes the SQL warehouse ID as an env var.

    APX's generated ``app.yml`` may only have the ``command`` field. The SQL
    dependency (``SqlConfig``) reads ``DATABRICKS_SQL_WAREHOUSE_ID`` from the
    environment, which must be injected via ``valueFrom`` referencing the
    ``sql-warehouse`` resource defined in ``databricks.yml``.
    """
    app_yml = app_dir / "app.yml"
    if not app_yml.exists():
        return

    config = yaml.safe_load(app_yml.read_text(encoding="utf-8"))
    if not isinstance(config, dict):
        return

    env_list: list[dict[str, str]] = config.get("env", [])
    if not isinstance(env_list, list):
        env_list = []

    # Add warehouse env var if missing
    has_warehouse = any(
        isinstance(e, dict) and e.get("name") == "DATABRICKS_SQL_WAREHOUSE_ID" for e in env_list
    )
    if not has_warehouse:
        env_list.append({"name": "DATABRICKS_SQL_WAREHOUSE_ID", "valueFrom": "sql-warehouse"})
        config["env"] = env_list
        app_yml.write_text(
            yaml.dump(config, default_flow_style=False, sort_keys=False),
            encoding="utf-8",
        )
        logger.info("Added DATABRICKS_SQL_WAREHOUSE_ID to app.yml")


def _disable_lakebase_dependency(context: ScaffoldContext, app_dir: Path) -> None:
    """Remove all Lakebase references from the app code.

    APX scaffolds Lakebase (Postgres) integration by default. Without a
    provisioned database, the app crashes on startup because
    ``DatabaseConfig`` requires ``PGAPPNAME`` env var. We remove lakebase
    imports from both ``core/__init__.py`` and ``core/dependencies.py``.
    """
    app_slug = context.app_name.replace("-", "_")
    core_dir = app_dir / "src" / app_slug / "backend" / "core"
    if not core_dir.exists():
        return

    # Remove lakebase lines from __init__.py and dependencies.py
    for filename in ("__init__.py", "dependencies.py"):
        filepath = core_dir / filename
        if not filepath.exists():
            continue
        content = filepath.read_text(encoding="utf-8")
        if "lakebase" not in content.lower():
            continue
        lines = content.splitlines(keepends=True)
        filtered: list[str] = []
        skip_next_docstring = False
        for ln in lines:
            if "lakebase" in ln.lower():
                skip_next_docstring = True
                continue
            # Skip orphaned docstrings that followed the removed line
            if skip_next_docstring and (
                ln.strip().startswith('"""') or ln.strip().startswith("Recommended")
            ):
                continue
            skip_next_docstring = False
            filtered.append(ln)
        filepath.write_text("".join(filtered), encoding="utf-8")
        logger.info("Removed Lakebase references from %s", filename)


def _postprocess_app_env(context: ScaffoldContext, app_dir: Path) -> None:
    """Ensure the app's .env has project-level vars for local dev."""
    env_path = app_dir / ".env"
    existing = env_path.read_text(encoding="utf-8") if env_path.exists() else ""

    additions: list[str] = []
    if "DATABRICKS_SQL_WAREHOUSE_ID" not in existing:
        additions.append(f"DATABRICKS_SQL_WAREHOUSE_ID={context.warehouse_id}")
    if "CATALOG" not in existing:
        additions.append(f"CATALOG={context.catalog}")
    if "DATABRICKS_CONFIG_PROFILE" not in existing and context.databricks_profile:
        additions.append(f"DATABRICKS_CONFIG_PROFILE={context.databricks_profile}")

    if additions:
        with env_path.open("a", encoding="utf-8") as f:
            f.write("\n# Added by exokit scaffold\n")
            for line in additions:
                f.write(f"{line}\n")
        logger.info("Added %d vars to app .env", len(additions))


def _install_shadcn_components(context: ScaffoldContext, app_dir: Path) -> None:
    """Pre-install common shadcn/ui components for the app."""
    app_slug = context.app_name.replace("-", "_")
    ui_dir = app_dir / "src" / app_slug / "ui"
    if not ui_dir.exists():
        logger.warning("UI directory not found at %s, skipping shadcn install", ui_dir)
        return

    components = [
        "button",
        "card",
        "table",
        "badge",
        "input",
        "skeleton",
        "dialog",
        "tabs",
        "select",
        "separator",
    ]
    cmd = ["npx", "shadcn@latest", "add", *components, "--yes"]

    try:
        result = subprocess.run(  # noqa: S603, S607
            cmd,
            cwd=str(ui_dir),
            capture_output=True,
            text=True,
            timeout=120,
        )
        if result.returncode == 0:
            logger.info("Pre-installed shadcn/ui components: %s", ", ".join(components))
        else:
            logger.warning("shadcn install returned non-zero: %s", result.stderr.strip()[:200])
    except (FileNotFoundError, subprocess.TimeoutExpired) as exc:
        logger.warning("Could not pre-install shadcn components: %s", exc)
