"""Plugin scaffolder.

Generates a new plugin skeleton from the templates under
shopdb/plugins/templates/. The generated plugin satisfies the framework
contract out of the box; tests/test_plugin_scaffold.py is the canary
that this stays true as the contract evolves.
"""

import re
from pathlib import Path
from string import Template
from typing import Optional


TEMPLATE_ROOT = Path(__file__).parent / 'templates'

VALID_NAME_PATTERN = re.compile(r'^[a-z][a-z0-9]*$')

RESERVED_NAMES = {
    'plugin', 'plugins', 'shopdb', 'core', 'api', 'tests', 'templates',
    'schemas', 'models', 'frontend', 'docs', 'migrations', 'scripts',
}


class ScaffoldError(Exception):
    """Raised when scaffolding cannot proceed."""


def validate_name(name: str) -> None:
    """Validate plugin name against CONTRIBUTING.md and reserved list.

    Raises ScaffoldError on any violation.
    """
    if not name:
        raise ScaffoldError('Plugin name is required')

    if not VALID_NAME_PATTERN.match(name):
        raise ScaffoldError(
            f'Plugin name "{name}" must be lowercase letters and digits only, '
            f'starting with a letter (no hyphens, underscores, or special chars). '
            f'See CONTRIBUTING.md for the naming convention.'
        )

    if name in RESERVED_NAMES:
        raise ScaffoldError(
            f'Plugin name "{name}" is reserved. Pick a different name.'
        )


def pascal_case(name: str) -> str:
    """Convert lowercase plugin name to PascalCase class name.

    'cameras' -> 'Cameras'. The convention assumes single-word lowercase
    plugin names per the naming rules in CONTRIBUTING.md, so this is
    just title-casing.
    """
    return name[:1].upper() + name[1:]


def _render_template(
    template_path: Path,
    out_path: Path,
    substitutions: dict,
    overwrite: bool,
) -> bool:
    """Render one template to out_path.

    Skips silently when out_path already exists and overwrite is False, so a
    scaffold never clobbers a file the author has already edited. Returns True
    when the file was written, False when it was skipped.
    """
    if out_path.exists() and not overwrite:
        return False
    out_path.parent.mkdir(parents=True, exist_ok=True)
    body = template_path.read_text()
    out_path.write_text(Template(body).safe_substitute(substitutions))
    return True


def _scaffold_frontend(
    name: str,
    substitutions: dict,
    plugin_target: Path,
    frontend_dir: Path,
    template_root: Path,
    overwrite: bool,
) -> None:
    """Render the frontend starting points for a scaffolded plugin.

    Writes the paste-in api-client snippet into the plugin directory, then the
    Vue views and the router route file into the real frontend tree. The
    snippet is emitted regardless of whether the frontend tree exists, because
    it is a plugin-directory artifact useful even for external-repo plugins.
    Views and the route file are skipped when frontend_dir is missing, which is
    the normal case for a plugin developed in its own repository.

    Ordering matters: every view is written before the route file. A route file
    that lazy-imports a view that is not on disk crashes the Vite dev server,
    so the views must land first.
    """
    fe_templates = template_root / 'frontend'
    if not fe_templates.exists():
        return

    plugin_name = substitutions['Name']

    # snippet lands in the plugin dir; author pastes it into api/index.js
    snippet_template = fe_templates / 'frontend-api-snippet.js.tmpl'
    if snippet_template.exists():
        _render_template(
            snippet_template,
            plugin_target / 'frontend-api-snippet.js',
            substitutions,
            overwrite,
        )

    # views and route need the real frontend tree; external repos skip these
    if not frontend_dir.exists():
        return

    views_dir = frontend_dir / 'views' / name

    # views first: route file lazy-imports them, missing views 500 vite
    view_templates = {
        'List.vue.tmpl': f'{plugin_name}List.vue',
        'Detail.vue.tmpl': f'{plugin_name}Detail.vue',
        'Form.vue.tmpl': f'{plugin_name}Form.vue',
    }
    for template_name, out_name in view_templates.items():
        template_path = fe_templates / 'views' / template_name
        if template_path.exists():
            _render_template(
                template_path,
                views_dir / out_name,
                substitutions,
                overwrite,
            )

    # route file last: all referenced views now exist on disk
    route_template = fe_templates / 'routes.js.tmpl'
    if route_template.exists():
        _render_template(
            route_template,
            frontend_dir / 'router' / 'routes' / f'{name}.js',
            substitutions,
            overwrite,
        )


def scaffold_plugin(
    name: str,
    description: str,
    plugins_dir: Path,
    template_root: Optional[Path] = None,
    overwrite: bool = False,
    frontend: bool = True,
    frontend_dir: Optional[Path] = None,
) -> Path:
    """Generate a new plugin from templates.

    Args:
        name: Plugin name (lowercase, single word)
        description: One-sentence description for manifest.json + README
        plugins_dir: Target plugins directory (e.g., <repo>/plugins)
        template_root: Override template source dir (default: bundled templates)
        overwrite: If True, overwrite an existing plugin directory and any
            existing generated frontend files
        frontend: If True, also render the Vue frontend starting points (list,
            detail, form views, a route file, and a paste-in api-client snippet)
        frontend_dir: Frontend src directory to render views/routes into
            (default: <plugins_dir>/../frontend/src). Views and the route file
            are skipped when this directory does not exist, which is the normal
            case for a plugin developed in its own repository.

    Returns:
        Path to the generated plugin directory.

    Raises:
        ScaffoldError on validation failure or when target exists and
        overwrite is False.
    """
    validate_name(name)

    template_root = template_root or TEMPLATE_ROOT
    if not template_root.exists():
        raise ScaffoldError(f'Template root not found: {template_root}')

    target = plugins_dir / name

    if target.exists():
        if not overwrite:
            raise ScaffoldError(
                f'Plugin directory already exists: {target}. '
                f'Pass overwrite=True or remove it first.'
            )

    substitutions = {
        'name': name,
        'Name': pascal_case(name),
        'description': description,
    }

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

    for template_path in template_root.rglob('*.tmpl'):
        rel = template_path.relative_to(template_root)

        # frontend templates render into the frontend tree, not the plugin dir
        if rel.parts and rel.parts[0] == 'frontend':
            continue

        out_rel_str = str(rel.with_suffix(''))
        if 'model.py' in out_rel_str:
            out_rel_str = out_rel_str.replace('model.py', f'{name}.py')

        out_path = target / out_rel_str
        out_path.parent.mkdir(parents=True, exist_ok=True)

        body = template_path.read_text()
        rendered = Template(body).safe_substitute(substitutions)

        out_path.write_text(rendered)

    if frontend:
        if frontend_dir is None:
            frontend_dir = plugins_dir.parent / 'frontend' / 'src'
        _scaffold_frontend(
            name=name,
            substitutions=substitutions,
            plugin_target=target,
            frontend_dir=Path(frontend_dir),
            template_root=template_root,
            overwrite=overwrite,
        )

    return target
