package config

import (
	"errors"
	"fmt"
	"path/filepath"
)

// PhraserConfig — the LLM-backed phraser seam. The daemon spawns llama-server
// as a managed subprocess and sends chat-completion requests to phrase nudge
// and reminder messages. nil ⇒ the template-based Stub is used instead.
//
// ModelPath is the only required field. cmd/mavend fills BinPath
// ("llama-server", found via PATH at spawn time) and Listen ("127.0.0.1:0", a
// random port read back off stderr) when they are empty.
//
// Nothing in this package defaults the rest, and an omitted numeric field
// reaches llama-server as a zero. That matters most for NGpuLayers: see below.
type PhraserConfig struct {
	ModelPath string `json:"model_path"`
	BinPath   string `json:"bin_path,omitempty"`
	Listen    string `json:"listen,omitempty"`

	// NGpuLayers — layers offloaded to the GPU, passed straight through as
	// `-ngl`. Omitted ⇒ 0, which is CPU-only inference.
	//
	// phraser.DefaultConfig says -1 (offload everything), but nothing calls it:
	// cmd/mavend builds a phraser.Config literally and copies this field across.
	// So the deploy config carries `"n_gpu_layers": 99` and must keep carrying
	// it. Vikunja has the discrepancy; do not "fix" it by writing a default
	// here, because that would change what a box without the key does.
	NGpuLayers int `json:"n_gpu_layers,omitempty"`

	// NCtx — context window, passed through as `-c`. Omitted ⇒ 0, which lets
	// llama-server pick. The resident model is a Thinking variant and needs
	// 4096; the deploy config sets it.
	NCtx int `json:"n_ctx,omitempty"`

	// Timeout — per-request budget. Omitted ⇒ phraser's own default.
	Timeout Duration `json:"timeout,omitempty"`

	// CacheRAMMiB bounds llama-server's prompt cache. Omitted ⇒ 512 MiB, which
	// is what keeps the resident model near 1 GB of RSS instead of the 7.9 GB
	// measured on 2026-08-03. Set it to -1 to pass no flag at all and let the
	// server apply its own 8 GiB default. See phraser.Config.CacheRAMMiB.
	CacheRAMMiB int `json:"cache_ram_mib,omitempty"`

	// LLMNudges — let the model word nudges again. Off by default: nudges are
	// worded from hand-written Russian templates now (the model broke the
	// persona and invented units). Chat, query and reminder phrasing always go
	// through the model regardless. See phraser.Config.LLMNudges.
	LLMNudges bool `json:"llm_nudges,omitempty"`

	// SwapModels — the gguf files the running daemon is allowed to swap to
	// without a restart (Vikunja #250). Empty (the default) means the swap
	// capability does not exist: ipc.MethodSwapModel answers ErrUnknownMethod,
	// exactly like an unconfigured weather or telegram block.
	//
	// It is an allowlist and not a directory on purpose. The request carries a
	// path, and llama-server is started with it as `-m`; anything short of an
	// exact match against a list a human wrote in this file would make "swap the
	// model" mean "load a file of your choosing off my disk". ModelPath is
	// always swappable back to whether or not it is listed.
	//
	// Paths must be absolute — the daemon's working directory is not the
	// operator's, and a relative path here would resolve somewhere surprising.
	SwapModels []string `json:"swap_models,omitempty"`
}

// validatePhraser refuses a block with no model, and a swap allowlist entry
// that would resolve somewhere other than where a reader of this file expects.
func (c *Config) validatePhraser() error {
	if c.Phraser == nil {
		return nil
	}
	if c.Phraser.ModelPath == "" {
		return errors.New("phraser.model_path is required")
	}
	// A relative entry in the swap allowlist would resolve against the
	// daemon's working directory, so the path a human reads in this file
	// would not be the path llama-server is handed. Fail at startup.
	for _, m := range c.Phraser.SwapModels {
		if !filepath.IsAbs(m) {
			return fmt.Errorf("phraser.swap_models: %q must be an absolute path", m)
		}
	}
	return nil
}
