"""Core logic for the ``mld init`` scaffolding command."""

from __future__ import annotations

import argparse
import re
import shutil
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path

from mld_sdk import init_templates as T


@dataclass(slots=True)
class PluginNames:
    """All derived name forms for a plugin."""

    human: str  # "Drug Response Analyzer"
    slug: str  # "drug-response-analyzer"
    package_name: str  # "mld-plugin-drug-response-analyzer"
    module_name: str  # "mld_plugin_drug_response_analyzer"
    class_name: str  # "DrugResponseAnalyzerPlugin"
    routes_prefix: str  # "/drug-response-analyzer"


def derive_names(human_name: str) -> PluginNames:
    """Derive all name forms from a human-readable plugin name."""
    # Normalize: strip leading/trailing whitespace
    name = human_name.strip()

    # Strip any existing "mld-plugin-" or "mld_plugin_" prefix to avoid doubling
    name = re.sub(r"^mld[-_]plugin[-_]", "", name, flags=re.IGNORECASE)

    if not name:
        raise ValueError("Plugin name cannot be empty.")

    # Build slug: lowercase, replace non-alnum with hyphens, collapse multiples
    slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
    if not slug:
        raise ValueError(
            "Plugin name must contain at least one alphanumeric character."
        )
    if slug[0].isdigit():
        raise ValueError(
            "Plugin name must start with a letter."
        )
    if not re.search(r"[a-z]", slug):
        raise ValueError("Plugin name must contain at least one letter.")

    package_name = f"mld-plugin-{slug}"
    module_name = package_name.replace("-", "_")

    # Build class name from slug segments for deterministic Python identifier output.
    class_name = "".join(part.capitalize() for part in slug.split("-")) + "Plugin"

    routes_prefix = f"/{slug}"

    return PluginNames(
        human=human_name.strip(),
        slug=slug,
        package_name=package_name,
        module_name=module_name,
        class_name=class_name,
        routes_prefix=routes_prefix,
    )


def _get_git_config(key: str) -> str:
    """Read a git config value, returning empty string on failure."""
    try:
        result = subprocess.run(
            ["git", "config", "--get", key],
            capture_output=True,
            text=True,
        )
        return result.stdout.strip() if result.returncode == 0 else ""
    except FileNotFoundError:
        return ""


def _get_sdk_version() -> str:
    """Get the current SDK version for dependency pinning."""
    try:
        from mld_sdk import __version__

        ver = __version__
        if ver and ver != "0.0.0" and "dev" not in ver:
            # Use major.minor.patch only
            match = re.match(r"(\d+\.\d+\.\d+)", ver)
            return match.group(1) if match else "0.9.0"
    except Exception:
        pass
    return "0.9.0"


def prompt_config(args: argparse.Namespace) -> dict[str, str]:
    """Gather configuration from args and interactive prompts."""
    config: dict[str, str] = {}

    # Directory
    directory = args.directory or "."
    config["directory"] = directory

    # Plugin name
    if args.name:
        config["name"] = args.name
    else:
        default_name = (
            directory.replace("-", " ").replace("_", " ").title()
            if directory != "."
            else "My Plugin"
        )
        try:
            answer = input(f"Plugin name [{default_name}]: ").strip()
        except (EOFError, KeyboardInterrupt):
            print()
            sys.exit(1)
        config["name"] = answer or default_name

    # Description
    if args.description:
        config["description"] = args.description
    else:
        default_desc = "An MLD analysis plugin"
        try:
            answer = input(f"Description [{default_desc}]: ").strip()
        except (EOFError, KeyboardInterrupt):
            print()
            sys.exit(1)
        config["description"] = answer or default_desc

    # Plugin type
    if args.type:
        config["type"] = args.type
    else:
        default_type = "analysis"
        try:
            answer = input(
                f"Plugin type (analysis/experiment-design) [{default_type}]: "
            ).strip()
        except (EOFError, KeyboardInterrupt):
            print()
            sys.exit(1)
        config["type"] = answer or default_type

    # Frontend
    if not args.no_frontend:
        try:
            answer = input("Include frontend? [Y/n]: ").strip().lower()
        except (EOFError, KeyboardInterrupt):
            print()
            sys.exit(1)
        config["include_frontend"] = "false" if answer == "n" else "true"
    else:
        config["include_frontend"] = "false"

    return config


def _build_template_context(
    names: PluginNames,
    config: dict[str, str],
) -> dict[str, str]:
    """Build the placeholder -> value mapping for template rendering."""
    plugin_type = config.get("type", "analysis")
    plugin_type_enum = (
        "EXPERIMENT_DESIGN" if plugin_type == "experiment-design" else "ANALYSIS"
    )

    return {
        "PLUGIN_NAME": names.human,
        "SLUG": names.slug,
        "PACKAGE_NAME": names.package_name,
        "MODULE_NAME": names.module_name,
        "CLASS_NAME": names.class_name,
        "ROUTES_PREFIX": names.routes_prefix,
        "DESCRIPTION": config.get("description", "An MLD analysis plugin"),
        "PLUGIN_TYPE": config.get("type", "analysis"),
        "PLUGIN_TYPE_ENUM": plugin_type_enum,
        "AUTHOR": _get_git_config("user.name") or "Your Name",
        "EMAIL": _get_git_config("user.email") or "you@example.com",
        "SDK_VERSION": _get_sdk_version(),
    }


def render(template: str, context: dict[str, str]) -> str:
    """Replace ``__KEY__`` placeholders in a template string."""
    result = template
    for key, value in context.items():
        result = result.replace(f"__{key.upper()}__", str(value))
    return result


# File tree: (relative_path, template_constant)
# None template means empty file.
BACKEND_FILES: list[tuple[str, str | None]] = [
    ("pyproject.toml", "PYPROJECT_TOML"),
    ("README.md", "README_MD"),
    ("CHANGELOG.md", "CHANGELOG_MD"),
    (".gitignore", "GITIGNORE"),
    ("src/__MODULE_NAME__/__init__.py", "INIT_PY"),
    ("src/__MODULE_NAME__/plugin.py", "PLUGIN_PY"),
    ("src/__MODULE_NAME__/routers/__init__.py", "ROUTERS_INIT_PY"),
    ("src/__MODULE_NAME__/routers/analysis.py", "ROUTERS_ANALYSIS_PY"),
    ("src/__MODULE_NAME__/schemas/__init__.py", "SCHEMAS_INIT_PY"),
    ("src/__MODULE_NAME__/schemas/requests.py", "SCHEMAS_REQUESTS_PY"),
    ("tests/__init__.py", None),
    ("tests/test_plugin.py", "TEST_PLUGIN_PY"),
    ("scripts/dev.sh", "SCRIPT_DEV_SH"),
    ("scripts/build.sh", "SCRIPT_BUILD_SH"),
    ("scripts/release.sh", "SCRIPT_RELEASE_SH"),
    (".github/workflows/ci.yml", "CI_YML"),
]

FRONTEND_FILES: list[tuple[str, str | None]] = [
    ("frontend/package.json", "FRONTEND_PACKAGE_JSON"),
    ("frontend/vite.config.ts", "FRONTEND_VITE_CONFIG_TS"),
    ("frontend/tsconfig.json", "FRONTEND_TSCONFIG_JSON"),
    ("frontend/tsconfig.node.json", "FRONTEND_TSCONFIG_NODE_JSON"),
    ("frontend/postcss.config.js", "FRONTEND_POSTCSS_CONFIG_JS"),
    ("frontend/index.html", "FRONTEND_INDEX_HTML"),
    ("frontend/src/main.ts", "FRONTEND_MAIN_TS"),
    ("frontend/src/style.css", "FRONTEND_STYLE_CSS"),
    ("frontend/src/env.d.ts", "FRONTEND_ENV_D_TS"),
    ("frontend/src/App.vue", "FRONTEND_APP_VUE"),
    ("frontend/src/views/DashboardView.vue", "FRONTEND_DASHBOARD_VIEW_VUE"),
    ("frontend/src/views/AnalysisView.vue", "FRONTEND_ANALYSIS_VIEW_VUE"),
]


def write_files(
    target_dir: Path,
    context: dict[str, str],
    include_frontend: bool,
) -> list[str]:
    """Write all scaffolded files. Returns list of relative paths created."""
    files = list(BACKEND_FILES)
    if include_frontend:
        files.extend(FRONTEND_FILES)

    # Also copy the sdk-auto-update workflow if the template exists
    sdk_auto_update = _find_sdk_auto_update_template()
    created: list[str] = []

    for rel_path, template_name in files:
        # Replace __MODULE_NAME__ in path
        rel_path = rel_path.replace("__MODULE_NAME__", context["MODULE_NAME"])
        abs_path = target_dir / rel_path

        abs_path.parent.mkdir(parents=True, exist_ok=True)

        if template_name is None:
            abs_path.write_text("")
        else:
            template = getattr(T, template_name)
            abs_path.write_text(render(template, context))

        created.append(rel_path)

    # Copy sdk-auto-update template if available
    if sdk_auto_update:
        dest = target_dir / ".github" / "workflows" / "sdk-auto-update.yml"
        dest.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(sdk_auto_update, dest)
        created.append(".github/workflows/sdk-auto-update.yml")

    return created


def _find_sdk_auto_update_template() -> Path | None:
    """Locate the sdk-auto-update workflow template in the SDK package."""
    # Check relative to this file (monorepo layout)
    candidate = (
        Path(__file__).resolve().parent.parent.parent.parent.parent
        / ".github"
        / "workflows"
        / "sdk-auto-update.template.yml"
    )
    if candidate.exists():
        return candidate
    return None


def post_scaffold(
    target_dir: Path,
    *,
    no_install: bool,
    no_git: bool,
    has_frontend: bool,
) -> None:
    """Run post-scaffolding steps: git init, install deps, chmod scripts."""
    # chmod scripts
    scripts_dir = target_dir / "scripts"
    if scripts_dir.exists():
        for script in scripts_dir.glob("*.sh"):
            script.chmod(script.stat().st_mode | 0o755)

    # git init
    if not no_git:
        if not (target_dir / ".git").exists():
            result = subprocess.run(
                ["git", "init"],
                cwd=target_dir,
                capture_output=True,
                text=True,
            )
            if result.returncode == 0:
                print("  Initialized git repository")
            else:
                print(f"  Warning: git init failed: {result.stderr}", file=sys.stderr)

    # uv sync
    if not no_install:
        if shutil.which("uv"):
            print("  Running uv sync...")
            result = subprocess.run(
                ["uv", "sync"],
                cwd=target_dir,
                capture_output=True,
                text=True,
            )
            if result.returncode != 0:
                print(
                    f"  Warning: uv sync failed: {result.stderr}",
                    file=sys.stderr,
                )
        else:
            print("  Warning: uv not found, skipping dependency install")

        # bun install
        if has_frontend:
            frontend_dir = target_dir / "frontend"
            if shutil.which("bun"):
                print("  Running bun install...")
                result = subprocess.run(
                    ["bun", "install"],
                    cwd=frontend_dir,
                    capture_output=True,
                    text=True,
                )
                if result.returncode != 0:
                    print(
                        f"  Warning: bun install failed: {result.stderr}",
                        file=sys.stderr,
                    )
            else:
                print("  Warning: bun not found, skipping frontend install")


def cmd_init(args: argparse.Namespace) -> None:
    """Execute the ``mld init`` command."""
    config = prompt_config(args)

    try:
        names = derive_names(config["name"])
    except ValueError as exc:
        print(f"Error: {exc}", file=sys.stderr)
        sys.exit(1)
    include_frontend = config.get("include_frontend", "true") == "true"

    # Resolve target directory
    directory = config["directory"]
    if directory == ".":
        target_dir = Path.cwd()
    else:
        target_dir = Path(directory).resolve()

    # Safety check: non-empty directory
    if target_dir.exists() and any(target_dir.iterdir()) and not args.force:
        print(
            f"Error: {target_dir} is not empty. Use --force to scaffold anyway.",
            file=sys.stderr,
        )
        sys.exit(1)

    target_dir.mkdir(parents=True, exist_ok=True)

    context = _build_template_context(names, config)
    print(f"\nCreating {names.package_name}/ ...")

    created = write_files(target_dir, context, include_frontend)

    for f in created:
        print(f"  {f}")

    print()
    post_scaffold(
        target_dir,
        no_install=args.no_install,
        no_git=args.no_git,
        has_frontend=include_frontend,
    )

    # Print next steps
    print(f"""
Done! Next steps:
  cd {directory}
  bash scripts/dev.sh
""")
