#!/usr/bin/env python3
"""Render ODS runtime config surfaces deterministically.

The first purpose of this script is read-only comparison: installers and
runtime mutators can ask what config should look like without writing files.
Follow-up wiring can then replace ad-hoc heredocs one surface at a time.
"""

from __future__ import annotations

import argparse
import json
import sys
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Callable


ROOT = Path(__file__).resolve().parents[1]
DEFAULT_MODEL = "qwen3.5-9b"
DEFAULT_GGUF = "Qwen3.5-9B-Q4_K_M.gguf"
DEFAULT_CONTEXT = 131072
DEFAULT_LITELLM_KEY = "sk-lemonade"
NO_KEY = "no-key"


@dataclass(frozen=True)
class RenderInputs:
    model: str
    gguf_file: str
    lemonade_model_id: str
    lemonade_api_base: str
    gpu_backend: str
    ods_mode: str
    llm_base_url: str
    litellm_key: str
    opencode_port: int
    context_length: int


@dataclass(frozen=True)
class RenderedFile:
    surface: str
    path: str
    content: str


def ensure_trailing_newline(text: str) -> str:
    return text if text.endswith("\n") else f"{text}\n"


def lemonade_model_id(inputs: RenderInputs) -> str:
    if inputs.lemonade_model_id:
        return inputs.lemonade_model_id
    return f"extra.{inputs.gguf_file}"


def hermes_model_id(inputs: RenderInputs) -> str:
    if inputs.ods_mode == "lemonade" or inputs.gpu_backend == "amd":
        return lemonade_model_id(inputs)
    return inputs.gguf_file or inputs.model


def opencode_key(inputs: RenderInputs) -> str:
    return inputs.litellm_key if inputs.ods_mode == "lemonade" else NO_KEY


def render_litellm_lemonade(inputs: RenderInputs) -> RenderedFile:
    model = lemonade_model_id(inputs)
    api_base = inputs.lemonade_api_base.rstrip("/") or "http://llama-server:8080/api/v1"
    content = f"""model_list:
  - model_name: default
    litellm_params:
      model: openai/{model}
      api_base: {api_base}
      api_key: {inputs.litellm_key}
      extra_body:
        chat_template_kwargs:
          enable_thinking: false

  - model_name: "*"
    litellm_params:
      model: openai/{model}
      api_base: {api_base}
      api_key: {inputs.litellm_key}
      extra_body:
        chat_template_kwargs:
          enable_thinking: false

litellm_settings:
  drop_params: true
  set_verbose: false
  request_timeout: 900
  stream_timeout: 900
"""
    return RenderedFile("litellm-lemonade", "config/litellm/lemonade.yaml", content)


def render_hermes(inputs: RenderInputs) -> RenderedFile:
    model = hermes_model_id(inputs)
    content = f"""model:
  default: "{model}"
  provider: "custom"
  base_url: "{inputs.llm_base_url}"
  context_length: {inputs.context_length}

auxiliary:
  compression:
    context_length: {inputs.context_length}

compression:
  enabled: true
  threshold: 0.75
  target_ratio: 0.50
  protect_last_n: 40
"""
    return RenderedFile("hermes", "data/hermes/config.yaml", content)


def render_perplexica(inputs: RenderInputs) -> RenderedFile:
    model = lemonade_model_id(inputs) if inputs.ods_mode == "lemonade" else (inputs.gguf_file or inputs.model)
    base_url = inputs.llm_base_url.rstrip("/") or "http://llama-server:8080"
    if not (base_url.endswith("/v1") or base_url.endswith("/api/v1")):
        base_url = f"{base_url}/v1"
    payload = {
        "modelProviders": [
            {
                "id": "openai",
                "type": "openai",
                "name": "ODS",
                "config": {
                    "apiKey": opencode_key(inputs),
                    "baseURL": base_url,
                },
                "chatModels": [{"key": model, "name": model}],
            }
        ],
        "preferences": {
            "defaultChatProvider": "openai",
            "defaultChatModel": model,
            "defaultEmbeddingProvider": "transformers",
            "defaultEmbeddingModel": "Xenova/all-MiniLM-L6-v2",
        },
        "setupComplete": True,
    }
    return RenderedFile(
        "perplexica",
        "data/perplexica/settings.seed.json",
        json.dumps(payload, indent=2, sort_keys=True) + "\n",
    )


def render_opencode(inputs: RenderInputs) -> RenderedFile:
    payload = {
        "provider": "openai-compatible",
        "baseURL": inputs.llm_base_url,
        "apiKey": opencode_key(inputs),
        "model": lemonade_model_id(inputs) if inputs.ods_mode == "lemonade" else inputs.model,
        "port": inputs.opencode_port,
    }
    return RenderedFile(
        "opencode",
        ".opencode/auth.json",
        json.dumps(payload, indent=2, sort_keys=True) + "\n",
    )


def render_env(inputs: RenderInputs) -> RenderedFile:
    lines = [
        f"ODS_MODE={inputs.ods_mode}",
        f"LLM_BACKEND={'lemonade' if inputs.ods_mode == 'lemonade' else 'llama-server'}",
        f"LLM_MODEL={inputs.model}",
        f"GGUF_FILE={inputs.gguf_file}",
        f"GPU_BACKEND={inputs.gpu_backend}",
        f"LLM_API_URL={inputs.llm_base_url}",
        f"CTX_SIZE={inputs.context_length}",
        f"MAX_CONTEXT={inputs.context_length}",
    ]
    return RenderedFile("env", ".env.generated", "\n".join(lines) + "\n")


RENDERERS: dict[str, Callable[[RenderInputs], RenderedFile]] = {
    "env": render_env,
    "opencode": render_opencode,
    "litellm-lemonade": render_litellm_lemonade,
    "perplexica": render_perplexica,
    "hermes": render_hermes,
}


def parse_args(argv: list[str]) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--surface", choices=["all", *sorted(RENDERERS)], default="all")
    parser.add_argument("--model", default=DEFAULT_MODEL)
    parser.add_argument("--gguf-file", default=DEFAULT_GGUF)
    parser.add_argument("--lemonade-model-id", default="")
    parser.add_argument("--lemonade-api-base", default="http://llama-server:8080/api/v1")
    parser.add_argument("--gpu-backend", choices=["amd", "apple", "cpu", "nvidia"], default="nvidia")
    parser.add_argument("--ods-mode", choices=["local", "cloud", "hybrid", "lemonade"], default="local")
    parser.add_argument("--llm-base-url", default="http://llama-server:8080/v1")
    parser.add_argument("--litellm-key", default=DEFAULT_LITELLM_KEY)
    parser.add_argument("--opencode-port", type=int, default=3003)
    parser.add_argument("--context-length", type=int, default=DEFAULT_CONTEXT)
    parser.add_argument("--format", choices=["json", "paths"], default="json")
    parser.add_argument("--output-root", default=".", help="Root directory used with --write")
    parser.add_argument("--write", action="store_true", help="Write rendered files under --output-root")
    return parser.parse_args(argv)


def select_surfaces(surface: str) -> list[str]:
    if surface == "all":
        return ["env", "opencode", "litellm-lemonade", "perplexica", "hermes"]
    return [surface]


def render(args: argparse.Namespace) -> dict[str, object]:
    inputs = RenderInputs(
        model=args.model,
        gguf_file=args.gguf_file,
        lemonade_model_id=args.lemonade_model_id,
        lemonade_api_base=args.lemonade_api_base,
        gpu_backend=args.gpu_backend,
        ods_mode=args.ods_mode,
        llm_base_url=args.llm_base_url,
        litellm_key=args.litellm_key,
        opencode_port=args.opencode_port,
        context_length=args.context_length,
    )
    files = [RENDERERS[name](inputs) for name in select_surfaces(args.surface)]
    written: list[str] = []
    if args.write:
        output_root = Path(args.output_root)
        for item in files:
            target = output_root / item.path
            target.parent.mkdir(parents=True, exist_ok=True)
            target.write_text(ensure_trailing_newline(item.content), encoding="utf-8")
            written.append(str(target))
    return {
        "version": "1",
        "mode": "write" if args.write else "dry-run",
        "inputs": asdict(inputs),
        "files": [asdict(RenderedFile(item.surface, item.path, ensure_trailing_newline(item.content))) for item in files],
        "written": written,
    }


def main(argv: list[str]) -> int:
    args = parse_args(argv)
    payload = render(args)
    if args.format == "paths":
        for item in payload["files"]:
            print(item["path"])
    else:
        print(json.dumps(payload, indent=2, sort_keys=True))
    return 0


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))
