// chatd is the LLM chat dispatcher service (Phase 4 — small-model
// pipeline tier abstraction). Routes POST /chat to the right
// provider based on the model-name prefix or :cloud suffix:
//
//	ollama/<m>          → local Ollama (no auth)
//	ollama_cloud/<m>    → Ollama Cloud (Bearer auth)
//	<m>:cloud           → Ollama Cloud (suffix variant)
//	openrouter/<v>/<m>  → OpenRouter (Bearer auth)
//	opencode/<m>        → OpenCode unified Zen+Go (Bearer auth)
//	kimi/<m>            → Kimi For Coding (Bearer auth)
//	bare names          → local Ollama (default)
//
// Provider keys come from env vars (or /etc/lakehouse/<provider>.env
// fallback files). Providers with empty keys stay unregistered, so
// requests for them 404 cleanly instead of 503-ing at call time.
//
// Routes:
//
//	POST /chat       — dispatch a chat request to the resolved provider
//	GET  /providers  — list registered providers (telemetry / health)
//	GET  /health     — readiness (always 200 — sub-providers are
//	                   independently checked via /providers)
package main

import (
	"encoding/json"
	"flag"
	"errors"
	"log/slog"
	"net/http"
	"os"
	"time"

	"github.com/go-chi/chi/v5"

	"git.agentview.dev/profit/golangLAKEHOUSE/internal/chat"
	"git.agentview.dev/profit/golangLAKEHOUSE/internal/shared"
)

const maxRequestBytes = 4 << 20 // 4 MiB cap on /chat bodies

func main() {
	configPath := flag.String("config", "lakehouse.toml", "path to TOML config")
	flag.Parse()

	cfg, err := shared.LoadConfig(*configPath)
	if err != nil {
		slog.Error("config", "err", err)
		os.Exit(1)
	}

	timeout := time.Duration(cfg.Chatd.TimeoutSecs) * time.Second
	registry := chat.BuildRegistry(chat.BuilderInput{
		OllamaURL:      cfg.Chatd.OllamaURL,
		OllamaCloudKey: chat.ResolveKey(cfg.Chatd.OllamaCloudKeyEnv, cfg.Chatd.OllamaCloudKeyFile),
		OpenRouterKey:  chat.ResolveKey(cfg.Chatd.OpenRouterKeyEnv, cfg.Chatd.OpenRouterKeyFile),
		OpenCodeKey:    chat.ResolveKey(cfg.Chatd.OpenCodeKeyEnv, cfg.Chatd.OpenCodeKeyFile),
		KimiKey:        chat.ResolveKey(cfg.Chatd.KimiKeyEnv, cfg.Chatd.KimiKeyFile),
		Timeout:        timeout,
	})

	h := &handlers{registry: registry}
	if err := shared.Run("chatd", cfg.Chatd.Bind, h.register, cfg.Auth); err != nil {
		slog.Error("server", "err", err)
		os.Exit(1)
	}
}

type handlers struct {
	registry *chat.Registry
}

func (h *handlers) register(r chi.Router) {
	// Routes mirror what the gateway proxies: /v1/chat → /chat (POST)
	// and /v1/chat/providers → /chat/providers (GET). Keeping providers
	// under /chat/ avoids a separate /providers root route that would
	// need its own gateway proxy entry.
	r.Post("/chat", h.handleChat)
	r.Get("/chat/providers", h.handleProviders)
}

func (h *handlers) handleChat(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestBytes)
	defer r.Body.Close()

	var req chat.Request
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid JSON: "+err.Error(), http.StatusBadRequest)
		return
	}
	if req.Model == "" || len(req.Messages) == 0 {
		http.Error(w, "model and messages are required", http.StatusBadRequest)
		return
	}

	resp, err := h.registry.Chat(r.Context(), req)
	if err != nil {
		writeChatError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, resp)
}

// handleProviders lists registered providers + per-provider Available()
// status. Phase 4 scrum fix B-2: looks up by name directly, skipping
// the prior synthetic-model-name route through Resolve. The endpoint
// reports registry membership, not routing rules — the latter is
// covered by /v1/chat itself.
func (h *handlers) handleProviders(w http.ResponseWriter, _ *http.Request) {
	names := h.registry.Names()
	statuses := make(map[string]bool, len(names))
	for _, n := range names {
		statuses[n] = h.registry.Available(n)
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"providers": statuses,
	})
}

// writeChatError maps chat sentinel errors to HTTP status codes.
// Unknown errors map to 500.
func writeChatError(w http.ResponseWriter, err error) {
	switch {
	case errors.Is(err, chat.ErrProviderNotFound):
		http.Error(w, err.Error(), http.StatusNotFound)
	case errors.Is(err, chat.ErrProviderDisabled):
		http.Error(w, err.Error(), http.StatusServiceUnavailable)
	case errors.Is(err, chat.ErrUpstream):
		http.Error(w, err.Error(), http.StatusBadGateway)
	case errors.Is(err, chat.ErrTimeout):
		http.Error(w, err.Error(), http.StatusGatewayTimeout)
	default:
		slog.Error("chat", "err", err)
		http.Error(w, "internal", http.StatusInternalServerError)
	}
}

func writeJSON(w http.ResponseWriter, status int, body any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	if err := json.NewEncoder(w).Encode(body); err != nil {
		slog.Error("encode", "err", err)
	}
}
