package main

import (
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"io/fs"
	"log"
	"net/http"
	"strings"

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

	"gitea.parodia.dev/drwily/petal/internal/config"
	"gitea.parodia.dev/drwily/petal/internal/db"
	"gitea.parodia.dev/drwily/petal/internal/docs"
	"gitea.parodia.dev/drwily/petal/internal/images"
	"gitea.parodia.dev/drwily/petal/internal/lexicon"
	"gitea.parodia.dev/drwily/petal/internal/llm"
	"gitea.parodia.dev/drwily/petal/internal/suggestions"
	"gitea.parodia.dev/drwily/petal/internal/tts"
	"gitea.parodia.dev/drwily/petal/internal/vocab"
	"gitea.parodia.dev/drwily/petal/web"
)

func main() {
	cfg := config.Load()

	database, err := db.Open(cfg.DatabasePath)
	if err != nil {
		log.Fatalf("database: %v", err)
	}
	defer database.Close()
	log.Printf("database ready at %s", cfg.DatabasePath)

	r := chi.NewRouter()
	r.Use(middleware.RequestID)
	r.Use(middleware.RealIP)
	r.Use(middleware.Logger)
	r.Use(middleware.Recoverer)

	// Build version: a hash of the embedded SPA shell. Vite rewrites index.html
	// with content-hashed asset names on every build, so this string changes
	// exactly when a new frontend is deployed — the client polls it to know when
	// to offer a refresh.
	version := buildVersion()
	log.Printf("frontend build version %s", version)

	r.Route("/api", func(api chi.Router) {
		api.Get("/health", func(w http.ResponseWriter, _ *http.Request) {
			w.Header().Set("Content-Type", "application/json")
			_, _ = w.Write([]byte(`{"status":"ok"}`))
		})

		api.Get("/version", func(w http.ResponseWriter, _ *http.Request) {
			w.Header().Set("Content-Type", "application/json")
			// Never cache: a stale cached version would defeat the whole check.
			w.Header().Set("Cache-Control", "no-store")
			_, _ = w.Write([]byte(`{"version":"` + version + `"}`))
		})

		llmClient := llm.NewLLMClient(cfg)
		sug := suggestions.New(database, llmClient)

		// Document CRUD plus the doc-scoped checkpoint/list suggestion routes,
		// both under /api/docs.
		docsHandler := docs.New(database)
		docsRouter := docsHandler.Routes()
		sug.RegisterDocRoutes(docsRouter)
		api.Mount("/docs", docsRouter)

		// Tag management (the roster) and cross-document full-text search.
		api.Mount("/tags", docsHandler.TagRoutes())
		api.Mount("/search", docsHandler.SearchRoutes())

		// Per-suggestion actions (accept/dismiss) under /api/suggestions.
		api.Mount("/suggestions", sug.Routes())

		// Offline lexicon: full word lookups (gloss + definition + synonyms) for
		// the right-click popover, and the lightweight Chinese-only gloss for the
		// inline hover/select tooltip. One handler so the datasets load once.
		lex := lexicon.NewHandler()
		api.Mount("/word", lex.Routes())
		api.Mount("/gloss", lex.GlossRoutes())

		// Vocabulary garden: words the writer looks up are captured here and
		// surfaced for gentle spaced-repetition review.
		api.Mount("/vocab", vocab.New(database).Routes())

		// Editor image uploads, stored on disk and served back by content hash.
		imgHandler, err := images.New(cfg.ImageDir)
		if err != nil {
			log.Fatalf("image store: %v", err)
		}
		api.Mount("/images", imgHandler.Routes())

		// Read-aloud: proxy short passages to a local Piper TTS server. Only
		// mounted when TTS_ENDPOINT is configured; otherwise the frontend falls
		// back to the browser's Web Speech API on its own.
		if ttsHandler, ok := tts.New(cfg); ok {
			api.Mount("/tts", ttsHandler.Routes())
			log.Printf("read-aloud enabled (TTS endpoint=%s)", cfg.TTSEndpoint)
		}
	})

	// Everything else: serve the embedded SPA (with index.html fallback for client routing).
	r.NotFound(spaHandler())

	addr := ":" + cfg.Port
	log.Printf("petal listening on %s (LLM backend=%s)", addr, cfg.LLMBackend)
	if err := http.ListenAndServe(addr, r); err != nil {
		log.Fatalf("server error: %v", err)
	}
}

// buildVersion derives a short, stable identifier for the currently embedded
// frontend by hashing dist/index.html. Vite stamps content-hashed asset names
// into that file each build, so the digest is a reliable "did the deploy
// change?" signal. Falls back to "dev" when the frontend hasn't been built.
func buildVersion() string {
	data, err := fs.ReadFile(web.DistFS, "dist/index.html")
	if err != nil {
		return "dev"
	}
	sum := sha256.Sum256(data)
	return hex.EncodeToString(sum[:])[:12]
}

// spaHandler serves the embedded web/dist as a single-page app: static files
// when they exist, falling back to index.html for unknown paths. If the
// frontend hasn't been built yet, it returns a friendly dev hint instead.
func spaHandler() http.HandlerFunc {
	sub, err := fs.Sub(web.DistFS, "dist")
	if err != nil {
		log.Fatalf("embed sub: %v", err)
	}

	if _, err := fs.Stat(sub, "index.html"); errors.Is(err, fs.ErrNotExist) {
		return func(w http.ResponseWriter, _ *http.Request) {
			w.Header().Set("Content-Type", "text/plain; charset=utf-8")
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write([]byte("Petal backend is running, but the frontend isn't built yet.\n" +
				"Run `npm run build` in web/, or use `npm run dev` for the dev server on :5173.\n"))
		}
	}

	fileServer := http.FileServer(http.FS(sub))
	return func(w http.ResponseWriter, req *http.Request) {
		p := strings.TrimPrefix(req.URL.Path, "/")
		if p == "" {
			p = "index.html"
		}
		if _, err := fs.Stat(sub, p); errors.Is(err, fs.ErrNotExist) {
			// Unknown path → let the SPA router handle it.
			req2 := new(http.Request)
			*req2 = *req
			req2.URL.Path = "/"
			fileServer.ServeHTTP(w, req2)
			return
		}
		fileServer.ServeHTTP(w, req)
	}
}
