"""Speks CLI — ``speks init`` and ``speks serve``."""

from __future__ import annotations

import os

# Suppress mkdocs-material warning about MkDocs 2.0 incompatibility.
os.environ.setdefault("NO_MKDOCS_2_WARNING", "1")

from pathlib import Path
from typing import Any

import typer
from rich.console import Console

from speks.i18n import set_locale, t

app = typer.Typer(
    name="speks",
    help="Speks — Interactive Functional Analysis Generator",
    add_completion=False,
)
console = Console()
err_console = Console(stderr=True)


def _init_locale(project_dir: Path) -> None:
    """Load config and set the i18n locale."""
    from speks.core.config import load_config

    config = load_config(project_dir)
    set_locale(config.locale)


# ---------------------------------------------------------------------------
# init
# ---------------------------------------------------------------------------

_SAMPLE_TOML = """\
[project]
name = "{name}"
src_dir = "src"
docs_dir = "docs"
diagrams_dir = "diagrams"
output_dir = "site"
theme_color = "#1976D2"
serve_port = 8000
locale = "en"
testcases_dir = "testcases"
# plantuml_server = "https://www.plantuml.com/plantuml"
# Use a custom server if the default is blocked:
# plantuml_server = "http://plantuml.internal.company.com"
# git_revisions = 3  # Number of git revisions to include (0 to disable)
"""

_SAMPLE_MKDOCS_YML = """\
site_name: "{name}"
docs_dir: "docs"
site_dir: "site"

theme:
  name: material
  logo: assets/logo-white.svg
  favicon: assets/logo.svg
  palette:
    primary: custom
  features:
    - navigation.instant
    - navigation.sections
    - navigation.top
    - search.suggest
    - content.code.copy

plugins:
  - search
  - speks-tags
  - speks-playground
  - speks-dependencies
  - speks-plantuml:
      server: "https://www.plantuml.com/plantuml"
  - speks-sequence
  - speks-versioning

markdown_extensions:
  - admonition
  - pymdownx.details
  - pymdownx.superfences:
      custom_fences:
        - name: mermaid
          class: mermaid
          format: !!python/name:pymdownx.superfences.fence_code_format
        - name: plantuml
          class: plantuml
          format: !!python/name:pymdownx.superfences.fence_code_format
  - pymdownx.highlight:
      anchor_linenums: true
  - pymdownx.inlinehilite
  - pymdownx.tabbed:
      alternate_style: true
  - tables
  - toc:
      permalink: true
"""

_SAMPLE_RULE = """\
from pydantic import BaseModel

from speks import ExternalService, MockResponse


class ClientBalance(BaseModel):
    \"\"\"Balance information returned by the Core Banking API.\"\"\"

    balance: float
    currency: str = "USD"
    account_status: str = "active"


class CheckClientBalance(ExternalService):
    \"\"\"Call to the Core Banking API (Blackbox).\"\"\"

    def execute(self, client_id: str) -> ClientBalance:
        # In production, performs a real HTTP call
        pass  # type: ignore[return-value]

    def mock(self, client_id: str) -> MockResponse:
        return MockResponse(data=ClientBalance(
            balance=1500.0,
            currency="USD",
            account_status="active",
        ))


class CreditDecision(BaseModel):
    \"\"\"Result of a credit evaluation.\"\"\"

    approved: bool
    balance: float
    reason: str


def evaluate_credit(client_id: str, amount: float) -> CreditDecision:
    \"\"\"Evaluate whether the client can obtain a credit for the requested amount.\"\"\"
    result = CheckClientBalance().call(client_id)
    approved = result.balance > amount
    return CreditDecision(
        approved=approved,
        balance=result.balance,
        reason="Approved" if approved else "Insufficient balance",
    )
"""

_SAMPLE_DOC = """\
# Credit Rules

## Credit Evaluation

The following rule determines whether a client can obtain a credit.

### Source Code

@[code](src/rules.py:evaluate_credit)

### Contract

@[contract](src/rules.py:evaluate_credit)

### External Service

@[code](src/rules.py:CheckClientBalance)

### Sequence Diagram

@[sequence](src/rules.py:evaluate_credit)

### Architecture Diagram

@[plantuml](diagrams/sequence.puml)

### Try it Live

@[playground](src/rules.py:evaluate_credit)
"""

_SAMPLE_INDEX = """\
# Welcome

This is an interactive functional analysis generated by **Speks**.

See the [Credit Rules](credit-rules.md) to get started.
"""

_SAMPLE_PUML = """\
@startuml
actor Analyst
participant "System" as sys
participant "Core Banking API" as api

Analyst -> sys : evaluate_credit(client_id, amount)
sys -> api : GET /balance/{client_id}
api --> sys : {balance: 1500.0}
sys --> Analyst : True / False
@enduml
"""


@app.command()
def init(
    name: str = typer.Argument("my-analysis-project", help="Project directory name"),
) -> None:
    """Scaffold a new Speks analysis workspace."""
    root = Path.cwd() / name
    if root.exists():
        console.print(f"[red]{t('cli.dir_exists', name=name)}[/red]")
        raise typer.Exit(1)

    (root / "src").mkdir(parents=True)
    (root / "docs").mkdir(parents=True)
    (root / "diagrams").mkdir(parents=True)

    # Make src a proper Python package so relative imports work
    (root / "src" / "__init__.py").touch()

    (root / "speks.toml").write_text(
        _SAMPLE_TOML.format(name=name), encoding="utf-8"
    )
    (root / "mkdocs.yml").write_text(
        _SAMPLE_MKDOCS_YML.format(name=name), encoding="utf-8"
    )
    (root / "src" / "rules.py").write_text(_SAMPLE_RULE, encoding="utf-8")
    (root / "docs" / "index.md").write_text(_SAMPLE_INDEX, encoding="utf-8")
    (root / "docs" / "credit-rules.md").write_text(_SAMPLE_DOC, encoding="utf-8")
    (root / "diagrams" / "sequence.puml").write_text(_SAMPLE_PUML, encoding="utf-8")

    # Copy packaged logos into the new project's docs/assets/
    from speks.web.builder import _get_packaged_asset

    import shutil

    assets_dir = root / "docs" / "assets"
    assets_dir.mkdir(parents=True, exist_ok=True)
    for logo_name in ("logo.svg", "logo-white.svg"):
        logo = _get_packaged_asset(logo_name)
        if logo is not None:
            shutil.copy2(logo, assets_dir / logo_name)

    console.print(f"[green]✔[/green] {t('cli.workspace_created')} [bold]{root}[/bold]")
    console.print(f"  {t('cli.run_hint')} [bold]cd {name} && speks serve[/bold]")


# ---------------------------------------------------------------------------
# serve
# ---------------------------------------------------------------------------


@app.command()
def serve(
    project_dir: str = typer.Argument(".", help="Path to the project workspace"),
    port: int = typer.Option(0, "--port", "-p", help="Port (0 = use config)"),
    develop: bool = typer.Option(False, "--develop", "-d", help="Watch source files and rebuild on changes"),
    revisions: int = typer.Option(
        0, "--revisions", "-r",
        help="Number of git revisions (0 = use config)",
    ),
) -> None:
    """Start the dev server (MkDocs site + FastAPI playground API)."""
    import uvicorn

    from speks.core.config import load_config
    from speks.web.builder import build_site
    from speks.web.server import create_app

    root = Path(project_dir).resolve()
    config = load_config(root)
    set_locale(config.locale)

    serve_port = port or config.serve_port

    console.print(t("cli.building_mkdocs"))
    site_dir = build_site(root, revisions_override=revisions or None)

    fastapi_app = create_app(root, site_dir)
    console.print(
        f"[green]✔[/green] {t('cli.serving_at')} [bold]http://localhost:{serve_port}[/bold]  {t('cli.stop_hint')}"
    )

    if develop:
        _serve_with_watch(fastapi_app, root, config, serve_port, revisions)
    else:
        uvicorn.run(fastapi_app, host="0.0.0.0", port=serve_port, log_level="warning")


def _serve_with_watch(
    fastapi_app: Any,
    root: Path,
    config: Any,
    serve_port: int,
    revisions: int,
) -> None:
    """Run uvicorn in a thread and watch for file changes to trigger rebuilds."""
    import threading

    import uvicorn
    from watchfiles import DefaultFilter, watch

    from speks.core.config import ProjectConfig
    from speks.web.builder import build_site

    cfg = config if isinstance(config, ProjectConfig) else None
    assert cfg is not None

    # Directories to watch
    watch_paths: list[Path] = []
    for d in (cfg.src_dir, cfg.docs_dir, cfg.diagrams_dir):
        p = root / d
        if p.is_dir():
            watch_paths.append(p)
    toml_path = root / "speks.toml"
    if toml_path.exists():
        watch_paths.append(toml_path)

    # Files generated by the build process that live inside watched dirs.
    # Ignore them to avoid an infinite rebuild loop.
    _GENERATED_ASSETS = {"logo.svg", "logo-white.svg", "speks.css"}
    docs_assets_dir = str((root / cfg.docs_dir / "assets").resolve())
    _default_filter = DefaultFilter()

    def _watch_filter(change: Any, path: str) -> bool:
        """Ignore build-generated assets inside docs/assets/."""
        if not _default_filter(change, path):
            return False
        if path.startswith(docs_assets_dir) and Path(path).name in _GENERATED_ASSETS:
            return False
        return True

    # Start uvicorn in a daemon thread
    server = uvicorn.Server(uvicorn.Config(
        fastapi_app, host="0.0.0.0", port=serve_port, log_level="warning",
    ))
    thread = threading.Thread(target=server.run, daemon=True)
    thread.start()

    console.print(
        f"[dim]{t('cli.watching', dirs=', '.join(str(p.relative_to(root)) for p in watch_paths))}[/dim]"
    )

    try:
        for changes in watch(*watch_paths, watch_filter=_watch_filter):
            changed = {Path(c[1]).name for c in changes}
            console.print(
                f"[yellow]↻[/yellow] {t('cli.rebuilding')} [dim]({', '.join(sorted(changed))})[/dim]"
            )
            try:
                build_site(root, revisions_override=revisions or None)
                console.print(f"[green]✔[/green] {t('cli.rebuild_done')}")
            except Exception as exc:
                err_console.print(f"[red]✘[/red] {t('cli.rebuild_error', error=exc)}")
    except KeyboardInterrupt:
        server.should_exit = True
        thread.join(timeout=3)


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    app()
