// atomizer-mock is the standalone placeholder Atomization Service (§5.1,
// §9.1). It calls Anthropic through the OpenAI-compatible chat-completions
// endpoint by default, or the native Messages API when
// LLM_API_STYLE=anthropic, and falls back to a deterministic equal split
// when no ANTHROPIC_API_KEY is configured so the whole system works offline.
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"
)

type config struct {
	port     string
	token    string
	apiKey   string
	baseURL  string
	model    string
	apiStyle string // openai | anthropic
}

func loadConfig() config {
	get := func(k, def string) string {
		if v := os.Getenv(k); v != "" {
			return v
		}
		return def
	}
	return config{
		port:     get("PORT", "8090"),
		token:    os.Getenv("ATOMIZER_TOKEN"),
		apiKey:   os.Getenv("ANTHROPIC_API_KEY"),
		baseURL:  strings.TrimRight(get("ANTHROPIC_OPENAI_BASE_URL", "https://api.anthropic.com/v1"), "/"),
		model:    get("ANTHROPIC_MODEL", "claude-sonnet-4-6"),
		apiStyle: get("LLM_API_STYLE", "openai"),
	}
}

type subTask struct {
	Title              string   `json:"title"`
	Description        string   `json:"description"`
	AcceptanceCriteria []string `json:"acceptanceCriteria"`
	EffortCoefficient  float64  `json:"effortCoefficient"`
}

type atomizeRequest struct {
	TaskID             string   `json:"taskId"`
	Title              string   `json:"title"`
	Description        string   `json:"description"`
	AcceptanceCriteria []string `json:"acceptanceCriteria"`
	Attachments        []struct {
		Name     string `json:"name"`
		URL      string `json:"url"`
		MimeType string `json:"mimeType"`
	} `json:"attachments"`
	Links           []string `json:"links"`
	SubdivisionNote string   `json:"subdivisionNote"`
	ExtensionNote   string   `json:"extensionNote"`
	Constraints     *struct {
		MinTasks int `json:"minTasks"`
		MaxTasks int `json:"maxTasks"`
	} `json:"constraints"`
}

type server struct {
	cfg config
	log *slog.Logger
	llm *llmClient
}

func main() {
	cfg := loadConfig()
	log := slog.New(slog.NewJSONHandler(os.Stdout, nil))
	s := &server{cfg: cfg, log: log, llm: newLLMClient(cfg, log)}

	mux := http.NewServeMux()
	mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
		writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
	})
	mux.HandleFunc("POST /v1/atomize", s.auth(s.handleAtomize))
	mux.HandleFunc("POST /v1/extend", s.auth(s.handleExtend))

	srv := &http.Server{Addr: ":" + cfg.port, Handler: mux, ReadHeaderTimeout: 10 * time.Second}
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()
	go func() {
		log.Info("atomizer-mock listening", "port", cfg.port,
			"llm", map[bool]string{true: "anthropic:" + cfg.apiStyle, false: "fallback (no api key)"}[cfg.apiKey != ""])
		if err := srv.ListenAndServe(); err != http.ErrServerClosed {
			log.Error("serve", "err", err)
			os.Exit(1)
		}
	}()
	<-ctx.Done()
	shutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	srv.Shutdown(shutCtx)
}

func writeJSON(w http.ResponseWriter, status int, v any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(v)
}

func writeErr(w http.ResponseWriter, status int, code, msg string) {
	writeJSON(w, status, map[string]any{"error": map[string]string{"code": code, "message": msg}})
}

func (s *server) auth(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if s.cfg.token != "" && r.Header.Get("Authorization") != "Bearer "+s.cfg.token {
			writeErr(w, http.StatusUnauthorized, "unauthorized", "missing or invalid bearer token")
			return
		}
		next(w, r)
	}
}

func (s *server) handleAtomize(w http.ResponseWriter, r *http.Request) {
	var req atomizeRequest
	if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<20)).Decode(&req); err != nil {
		writeErr(w, http.StatusBadRequest, "bad_request", err.Error())
		return
	}
	minT, maxT := 2, 8
	if req.Constraints != nil {
		if req.Constraints.MinTasks > 0 {
			minT = req.Constraints.MinTasks
		}
		if req.Constraints.MaxTasks > 0 {
			maxT = req.Constraints.MaxTasks
		}
	}
	if minT > maxT {
		minT = maxT
	}

	tasks, model, notes, err := s.llm.atomize(r.Context(), req, minT, maxT)
	if err != nil {
		s.log.Error("atomize failed", "taskId", req.TaskID, "err", err)
		writeErr(w, http.StatusBadGateway, "llm_failed", err.Error())
		return
	}
	normalizeSum(tasks)
	writeJSON(w, http.StatusOK, map[string]any{"tasks": tasks, "model": model, "notes": notes})
}

func (s *server) handleExtend(w http.ResponseWriter, r *http.Request) {
	var req atomizeRequest
	if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<20)).Decode(&req); err != nil {
		writeErr(w, http.StatusBadRequest, "bad_request", err.Error())
		return
	}
	if strings.TrimSpace(req.ExtensionNote) == "" {
		writeErr(w, http.StatusBadRequest, "note_required", "extensionNote is required")
		return
	}
	task, model, notes, err := s.llm.extend(r.Context(), req)
	if err != nil {
		s.log.Error("extend failed", "taskId", req.TaskID, "err", err)
		writeErr(w, http.StatusBadGateway, "llm_failed", err.Error())
		return
	}
	if task.EffortCoefficient <= 0 || task.EffortCoefficient > 2 {
		task.EffortCoefficient = 0.3
	}
	writeJSON(w, http.StatusOK, map[string]any{"task": task, "model": model, "notes": notes})
}

// normalizeSum forces coefficients to sum to exactly 1.0.
func normalizeSum(tasks []subTask) {
	sum := 0.0
	for _, t := range tasks {
		sum += t.EffortCoefficient
	}
	if sum <= 0 {
		eq := 1.0 / float64(len(tasks))
		for i := range tasks {
			tasks[i].EffortCoefficient = eq
		}
		sum = 1.0
	}
	total := 0.0
	largest := 0
	for i := range tasks {
		tasks[i].EffortCoefficient = round4(tasks[i].EffortCoefficient / sum)
		total += tasks[i].EffortCoefficient
		if tasks[i].EffortCoefficient > tasks[largest].EffortCoefficient {
			largest = i
		}
	}
	tasks[largest].EffortCoefficient = round4(tasks[largest].EffortCoefficient + 1 - total)
}

func round4(v float64) float64 {
	return float64(int64(v*10000+0.5)) / 10000
}

func fallbackAtomize(req atomizeRequest, minT, maxT int) []subTask {
	n := (minT + maxT) / 2
	if n < 1 {
		n = 3
	}
	out := make([]subTask, n)
	eq := round4(1.0 / float64(n))
	for i := range out {
		out[i] = subTask{
			Title:              fmt.Sprintf("%s — part %d of %d", req.Title, i+1, n),
			Description:        fmt.Sprintf("Deterministic offline split %d/%d of:\n\n%s", i+1, n, req.Description),
			AcceptanceCriteria: append([]string{}, req.AcceptanceCriteria...),
			EffortCoefficient:  eq,
		}
	}
	return out
}

func fallbackExtend(req atomizeRequest) subTask {
	return subTask{
		Title:              req.Title + " — extension: " + firstLine(req.ExtensionNote, 60),
		Description:        "Deterministic offline extension of the source task.\n\nRequested scope: " + req.ExtensionNote,
		AcceptanceCriteria: []string{"extension scope implemented: " + firstLine(req.ExtensionNote, 120)},
		EffortCoefficient:  0.3,
	}
}

func firstLine(s string, max int) string {
	if i := strings.IndexByte(s, '\n'); i >= 0 {
		s = s[:i]
	}
	if len(s) > max {
		s = s[:max]
	}
	return s
}
