// Command ai-bot is a plaintext Matrix bot user (@ai) that answers xAI Grok
// completions in its rooms: @-mentions in group rooms and every message in a
// 1:1. It runs as a Synapse application service — Synapse pushes transactions to
// its HTTP endpoint — and talks the Matrix CS-API over plain HTTP (no Olm/Megolm,
// Vojo rooms are unencrypted by default), calling the xAI OpenAI-compatible Chat
// Completions API. See README.md and docs/plans/grok_bot.md.
package main

import (
	"context"
	"fmt"
	"os"
	"os/signal"
	"strings"
	"syscall"
)

func main() {
	logger := newLogger()

	// `ai-bot generate-registration` writes a fresh registration.yaml with random
	// tokens (the mautrix bridge idiom), then exits. Runs BEFORE LoadConfig — the
	// tokens don't exist yet. Inputs: BOT_MXID (required), REGISTRATION_PATH
	// (default /data/registration.yaml), AS_URL (default http://ai-bot:8009).
	if len(os.Args) > 1 && os.Args[1] == "generate-registration" {
		mxid := getenv("BOT_MXID", "")
		if mxid == "" {
			logger.Error("BOT_MXID is required to generate the registration")
			os.Exit(1)
		}
		path := getenv("REGISTRATION_PATH", "/data/registration.yaml")
		asURL := getenv("AS_URL", "http://ai-bot:8009")
		if err := GenerateRegistration(path, asURL, localpartOf(mxid), serverOf(mxid)); err != nil {
			logger.Error("generate-registration failed", "err", err)
			os.Exit(1)
		}
		fmt.Printf("wrote %s\n", path)
		fmt.Println("Next: mount this file into Synapse, add it to app_service_config_files,")
		fmt.Println("and restart Synapse. The bot reads its tokens from this file (set")
		fmt.Println("REGISTRATION_PATH to the same path in the bot's environment).")
		return
	}

	cfg, err := LoadConfig()
	if err != nil {
		logger.Error("config error", "err", err)
		os.Exit(1)
	}

	// Load the system prompt up front so a missing/unreadable file fails fast
	// at startup rather than on the first message.
	promptBytes, err := os.ReadFile(cfg.SystemPromptPath)
	if err != nil {
		logger.Error("cannot read system prompt", "path", cfg.SystemPromptPath, "err", err)
		os.Exit(1)
	}
	cfg.SystemPrompt = string(promptBytes)

	// Load the curated project KB the same way (fail fast at startup, not on the first
	// product question) when the route is enabled. LoadConfig already required the path; here
	// we read it and reject an empty file (fail-closed — an empty KB would ground nothing).
	// A KB much larger than the prompt budget is also refused so it can't blow maxPromptTokens
	// (insertSystemNote adds it AFTER history truncation). Off → ProjectKB stays "".
	if cfg.ProjectKBEnabled {
		kbBytes, err := os.ReadFile(cfg.ProjectKBPath)
		if err != nil {
			logger.Error("cannot read project KB", "path", cfg.ProjectKBPath, "err", err)
			os.Exit(1)
		}
		cfg.ProjectKB = string(kbBytes)
		if strings.TrimSpace(cfg.ProjectKB) == "" {
			logger.Error("PROJECT_KB_PATH is empty", "path", cfg.ProjectKBPath)
			os.Exit(1)
		}
		if t := estimateTokens(cfg.ProjectKB); t > maxProjectKBTokens {
			logger.Error("project KB is too large for the prompt budget",
				"path", cfg.ProjectKBPath, "est_tokens", t, "max", maxProjectKBTokens)
			os.Exit(1)
		}
	}

	// `ai-bot check-config` validates env + prompt + state dir and exits 0.
	// Used by the A1 acceptance check ("container starts, reads env") and as a
	// cheap operator smoke test without touching the homeserver.
	if len(os.Args) > 1 && os.Args[1] == "check-config" {
		fmt.Println(cfg.Summary())
		fmt.Printf("  SYSTEM_PROMPT      = loaded (%d bytes)\n", len(cfg.SystemPrompt))
		if cfg.ProjectKBEnabled {
			fmt.Printf("  PROJECT_KB         = loaded (%d bytes, ~%d tokens)\n", len(cfg.ProjectKB), estimateTokens(cfg.ProjectKB))
		}
		fmt.Println("config OK")
		return
	}

	if err := os.MkdirAll(cfg.StateDir, 0o700); err != nil {
		logger.Error("cannot create state dir", "path", cfg.StateDir, "err", err)
		os.Exit(1)
	}

	fmt.Fprintf(os.Stderr, "%s\n", cfg.Summary())
	logger.Info("starting Vojo AI bot")

	// Cancel on SIGINT/SIGTERM so the transaction server shuts down cleanly.
	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer stop()

	bot, err := NewBot(ctx, cfg, logger)
	if err != nil {
		logger.Error("startup failed", "err", err)
		os.Exit(1)
	}
	defer bot.Close()

	if err := bot.Run(ctx); err != nil && ctx.Err() == nil {
		logger.Error("appservice server exited", "err", err)
		os.Exit(1)
	}
	logger.Info("shut down cleanly")
}
