// Package llm is the Phase 12 conversational layer. The Command Center
// optionally runs a local Ollama instance and exposes a conversational
// interface grounded in the operator's actual data via structured tool
// calls.
package llm

import (
	"bytes"
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"
)

// Client talks to an Ollama HTTP API.
type Client struct {
	baseURL string
	model   string
	http    *http.Client
}

// Config is the constructor shape.
type Config struct {
	BaseURL string
	Model   string
	HTTP    *http.Client
}

// New constructs a client.
func New(cfg Config) (*Client, error) {
	if cfg.BaseURL == "" {
		cfg.BaseURL = "http://127.0.0.1:11434"
	}
	if cfg.Model == "" {
		cfg.Model = "llama3.1:8b"
	}
	hc := cfg.HTTP
	if hc == nil {
		hc = &http.Client{Timeout: 180 * time.Second}
	}
	return &Client{baseURL: strings.TrimRight(cfg.BaseURL, "/"), model: cfg.Model, http: hc}, nil
}

// Health pings the Ollama instance.
func (c *Client) Health(ctx context.Context) error {
	req, _ := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/api/tags", nil)
	resp, err := c.http.Do(req)
	if err != nil {
		return err
	}
	_ = resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("llm: ollama status %d", resp.StatusCode)
	}
	return nil
}

// Message is one entry in the conversation history.
type Message struct {
	Role      string  `json:"role"`
	Content   string  `json:"content"`
	ToolCalls []Tool  `json:"tool_calls,omitempty"`
}

// Tool models an Ollama-style tool call (compatible with the OpenAI shape).
type Tool struct {
	Function struct {
		Name      string         `json:"name"`
		Arguments map[string]any `json:"arguments"`
	} `json:"function"`
}

// chatRequest mirrors Ollama's /api/chat shape.
type chatRequest struct {
	Model    string                   `json:"model"`
	Messages []Message                `json:"messages"`
	Stream   bool                     `json:"stream"`
	Tools    []map[string]any         `json:"tools,omitempty"`
}

// chatResponse mirrors Ollama's /api/chat shape (non-streamed).
type chatResponse struct {
	Message Message `json:"message"`
	Done    bool    `json:"done"`
}

// SystemPromptTemplate is the default operator-customizable prompt. It
// matches PROJECT.md Appendix F minus the per-deployment edits an operator
// might make in `config/llm-system-prompt.yaml`.
const SystemPromptTemplate = `You are the Seedbox Command Center's data-grounded assistant. The operator
asks questions about their seedbox; you answer using the tools available to
you to query their actual data. Never speculate or pattern-match from prior
knowledge about seedbox best practices when you have a tool that can give
you the actual answer.

Goals:
1. Answer the operator's question accurately based on their data.
2. Show your work briefly when the answer required non-obvious analysis.
3. Surface caveats when the data is incomplete or stale.
4. Suggest a concrete next action when the situation warrants one.

Style: short answers unless detail is requested. Don't claim to have taken
actions you haven't. If a question requires an action (pause torrent, change
filter), describe it and ask the operator to confirm in the UI.`

// Chat sends a one-shot chat completion and returns the assistant's reply.
// Streaming is left to Phase 12 polish; the operator-facing UX is fine with
// a single-shot blocking call at single-operator scale.
func (c *Client) Chat(ctx context.Context, history []Message, tools []map[string]any) (Message, error) {
	if len(history) == 0 {
		return Message{}, errors.New("llm: empty history")
	}
	body, _ := json.Marshal(chatRequest{
		Model:    c.model,
		Messages: history,
		Stream:   false,
		Tools:    tools,
	})
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/api/chat", bytes.NewReader(body))
	if err != nil {
		return Message{}, err
	}
	req.Header.Set("Content-Type", "application/json")
	resp, err := c.http.Do(req)
	if err != nil {
		return Message{}, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		b, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
		return Message{}, fmt.Errorf("llm: status %d: %s", resp.StatusCode, b)
	}
	var out chatResponse
	if err := json.NewDecoder(io.LimitReader(resp.Body, 32<<20)).Decode(&out); err != nil {
		return Message{}, err
	}
	return out.Message, nil
}

// --- Persistence wrapper -----------------------------------------------------

// Store wraps llm_conversations + llm_messages.
type Store struct {
	db *sql.DB
}

// NewStore constructs a Store.
func NewStore(db *sql.DB) *Store { return &Store{db: db} }

// StartConversation inserts a new conversation row and returns its id.
func (s *Store) StartConversation(ctx context.Context, title string) (int64, error) {
	now := time.Now().Unix()
	res, err := s.db.ExecContext(ctx,
		`INSERT INTO llm_conversations(started_at, last_message_at, title) VALUES (?, ?, ?)`,
		now, now, nullText(title))
	if err != nil {
		return 0, err
	}
	return res.LastInsertId()
}

// AppendMessage saves one message to the conversation. tool_calls_json may
// be empty.
func (s *Store) AppendMessage(ctx context.Context, convID int64, role, content string, toolCalls []Tool) error {
	tcJSON := ""
	if len(toolCalls) > 0 {
		b, _ := json.Marshal(toolCalls)
		tcJSON = string(b)
	}
	now := time.Now().Unix()
	tx, err := s.db.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer func() { _ = tx.Rollback() }()
	if _, err := tx.ExecContext(ctx,
		`INSERT INTO llm_messages(conversation_id, role, content, tool_calls_json, timestamp) VALUES (?, ?, ?, ?, ?)`,
		convID, role, content, nullText(tcJSON), now); err != nil {
		return err
	}
	if _, err := tx.ExecContext(ctx,
		`UPDATE llm_conversations SET last_message_at = ? WHERE id = ?`, now, convID); err != nil {
		return err
	}
	return tx.Commit()
}

// History returns the conversation's messages in chronological order.
func (s *Store) History(ctx context.Context, convID int64) ([]Message, error) {
	rows, err := s.db.QueryContext(ctx, `
		SELECT role, content, tool_calls_json FROM llm_messages
		WHERE conversation_id = ? ORDER BY timestamp ASC, id ASC
	`, convID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []Message
	for rows.Next() {
		var m Message
		var tcJSON sql.NullString
		if err := rows.Scan(&m.Role, &m.Content, &tcJSON); err != nil {
			return nil, err
		}
		if tcJSON.Valid {
			_ = json.Unmarshal([]byte(tcJSON.String), &m.ToolCalls)
		}
		out = append(out, m)
	}
	return out, rows.Err()
}

// Conversation is the JSON-friendly summary.
type Conversation struct {
	ID            int64  `json:"id"`
	StartedAt     int64  `json:"started_at"`
	LastMessageAt int64  `json:"last_message_at"`
	Title         string `json:"title,omitempty"`
	Pinned        bool   `json:"pinned"`
}

// ListConversations returns recent conversations pinned-first.
func (s *Store) ListConversations(ctx context.Context, limit int) ([]Conversation, error) {
	if limit <= 0 || limit > 500 {
		limit = 50
	}
	rows, err := s.db.QueryContext(ctx, `
		SELECT id, started_at, last_message_at, title, pinned
		FROM llm_conversations
		ORDER BY pinned DESC, last_message_at DESC LIMIT ?
	`, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []Conversation
	for rows.Next() {
		var c Conversation
		var title sql.NullString
		var pinned int
		if err := rows.Scan(&c.ID, &c.StartedAt, &c.LastMessageAt, &title, &pinned); err != nil {
			return nil, err
		}
		if title.Valid {
			c.Title = title.String
		}
		c.Pinned = pinned != 0
		out = append(out, c)
	}
	return out, rows.Err()
}

func nullText(s string) any {
	if s == "" {
		return nil
	}
	return s
}
