// Package zotcore is the public Go SDK for embedding the zot agent
// runtime in third-party programs. It is the only stable, importable
// surface the project exposes; everything under internal/ is subject
// to change without notice.
//
// Lifecycle:
//
//	rt, err := zotcore.New(zotcore.Config{Provider: "anthropic", Model: "claude-sonnet-4-5"})
//	if err != nil { ... }
//	defer rt.Close()
//
//	events, err := rt.Prompt(ctx, "fix the failing test", nil)
//	if err != nil { ... }
//	for ev := range events {
//	    fmt.Printf("%s: %+v\n", ev.Type(), ev)
//	}
//
// Concurrency model: one prompt at a time per Runtime. Spawn one
// Runtime per project / cwd. The Cancel call interrupts the active
// prompt; subsequent prompts work normally.
//
// For a non-Go consumer, run `zot rpc` and speak the same JSON
// schema over stdin/stdout. See docs/rpc.md.
package zotcore

import (
	"context"
	"errors"
	"fmt"
	"sync"

	"github.com/patriceckhart/zot/internal/agent"
	"github.com/patriceckhart/zot/internal/core"
	"github.com/patriceckhart/zot/internal/provider"
)

// Config configures a Runtime. All fields are optional; sensible
// defaults are read from $ZOT_HOME/config.json, env vars, and the
// resolver chain (the same one the cli uses).
type Config struct {
	// Provider is "anthropic" or "openai". Empty = use the user's
	// default from config.json or env.
	Provider string

	// Model is the model id. Empty = use the provider's default.
	Model string

	// CWD is the working directory the agent operates in. Tools are
	// confined to this directory when Lock is true. Empty = current
	// process cwd.
	CWD string

	// SystemPrompt overrides the built-in system prompt.
	SystemPrompt string

	// AppendSystemPrompt is appended to the built-in (or overridden)
	// system prompt. Useful for project-specific instructions.
	AppendSystemPrompt []string

	// Reasoning sets the reasoning effort for models that support it
	// ("low", "medium", "high"). Empty = no reasoning.
	Reasoning string

	// MaxSteps caps the agent loop iterations per Prompt call.
	// 0 uses the default (50).
	MaxSteps int

	// APIKey overrides the credential lookup chain.
	APIKey string

	// BaseURL overrides the provider base url (for tests / proxies).
	BaseURL string

	// Tools is the list of tools to enable. Nil/empty = all four
	// (read, write, edit, bash). Pass an empty-but-non-nil slice
	// (e.g. []string{}) plus NoTools=true to disable everything.
	Tools []string

	// NoTools disables every tool. Useful for chat-only embeddings.
	NoTools bool

	// Lock confines tools to CWD. Same effect as the /jail command.
	Lock bool
}

// Runtime is one zot agent session. Safe for use from one goroutine
// at a time per Runtime; create separate Runtimes for parallel work.
type Runtime struct {
	mu       sync.Mutex
	agent    *core.Agent
	provider string
	model    string
	cwd      string

	// activeCancel is set while a Prompt is streaming.
	activeCancel context.CancelFunc

	// closed signals that Close has been called.
	closed bool
}

// New constructs a Runtime from cfg. Returns an error if no
// credential is available for the requested provider.
func New(cfg Config) (*Runtime, error) {
	args := agent.Args{
		Mode:               agent.ModeJSON, // headless
		Provider:           cfg.Provider,
		Model:              cfg.Model,
		CWD:                cfg.CWD,
		APIKey:             cfg.APIKey,
		BaseURL:            cfg.BaseURL,
		SystemPrompt:       cfg.SystemPrompt,
		AppendSystemPrompt: cfg.AppendSystemPrompt,
		Reasoning:          cfg.Reasoning,
		MaxSteps:           cfg.MaxSteps,
		Tools:              cfg.Tools,
		NoTools:            cfg.NoTools,
		NoSess:             true, // SDK callers manage persistence themselves
	}
	if args.MaxSteps == 0 {
		args.MaxSteps = 50
	}
	r, err := agent.Resolve(args, true)
	if err != nil {
		return nil, err
	}
	if cfg.Lock && r.Sandbox != nil {
		r.Sandbox.Lock()
	}
	ag := r.NewAgent()
	return &Runtime{
		agent:    ag,
		provider: r.Provider,
		model:    r.Model,
		cwd:      r.CWD,
	}, nil
}

// Provider returns the active provider id.
func (r *Runtime) Provider() string { r.mu.Lock(); defer r.mu.Unlock(); return r.provider }

// Model returns the active model id.
func (r *Runtime) Model() string { r.mu.Lock(); defer r.mu.Unlock(); return r.model }

// CWD returns the working directory the agent operates in.
func (r *Runtime) CWD() string { r.mu.Lock(); defer r.mu.Unlock(); return r.cwd }

// Messages returns a copy of the current transcript.
func (r *Runtime) Messages() []Message {
	if r.agent == nil {
		return nil
	}
	src := r.agent.Messages()
	out := make([]Message, len(src))
	for i, m := range src {
		out[i] = Message{
			Role:    string(m.Role),
			Content: convertContent(m.Content),
			Time:    m.Time.Format("2006-01-02T15:04:05Z"),
		}
	}
	return out
}

// SetMessages replaces the transcript. Use to seed history from a
// session file or to clear with nil.
func (r *Runtime) SetMessages(msgs []Message) {
	if r.agent == nil {
		return
	}
	out := make([]provider.Message, 0, len(msgs))
	for _, m := range msgs {
		out = append(out, provider.Message{
			Role:    provider.Role(m.Role),
			Content: rebuildContent(m.Content),
		})
	}
	r.agent.SetMessages(out)
}

// Cost returns the cumulative usage and dollar cost for this Runtime.
func (r *Runtime) Cost() Usage {
	if r.agent == nil {
		return Usage{}
	}
	u := r.agent.Cost()
	return Usage{
		Input:      u.InputTokens,
		Output:     u.OutputTokens,
		CacheRead:  u.CacheReadTokens,
		CacheWrite: u.CacheWriteTokens,
		CostUSD:    u.CostUSD,
	}
}

// Prompt sends a user message and runs the agent loop. Returns a
// channel that emits one Event per agent action, closed when the
// turn finishes (cleanly or with error). Only one Prompt may be
// active at a time per Runtime; concurrent calls return ErrBusy.
func (r *Runtime) Prompt(ctx context.Context, text string, images []Image) (<-chan Event, error) {
	r.mu.Lock()
	if r.closed {
		r.mu.Unlock()
		return nil, ErrClosed
	}
	if r.activeCancel != nil {
		r.mu.Unlock()
		return nil, ErrBusy
	}
	if r.agent == nil {
		r.mu.Unlock()
		return nil, fmt.Errorf("zotcore: no agent (login first via /login or set credentials)")
	}
	subCtx, cancel := context.WithCancel(ctx)
	r.activeCancel = cancel
	r.mu.Unlock()

	imgBlocks := make([]provider.ImageBlock, 0, len(images))
	for _, img := range images {
		imgBlocks = append(imgBlocks, provider.ImageBlock{MimeType: img.MimeType, Data: img.Data})
	}

	out := make(chan Event, 16)
	go func() {
		defer close(out)
		defer func() {
			r.mu.Lock()
			r.activeCancel = nil
			r.mu.Unlock()
		}()
		err := r.agent.Prompt(subCtx, text, imgBlocks, func(ev core.AgentEvent) {
			out <- toEvent(ev)
		})
		if err != nil && !errors.Is(err, context.Canceled) {
			out <- Event{Type: "error", Error: err.Error()}
		}
	}()
	return out, nil
}

// Cancel interrupts the active Prompt, if any. Safe to call when no
// prompt is in flight (no-op).
func (r *Runtime) Cancel() {
	r.mu.Lock()
	cancel := r.activeCancel
	r.mu.Unlock()
	if cancel != nil {
		cancel()
	}
}

// Compact summarises the current transcript into a single synthetic
// user message, freeing up context. Blocks until done.
func (r *Runtime) Compact(ctx context.Context, customInstructions string) (CompactResult, error) {
	if r.agent == nil {
		return CompactResult{}, fmt.Errorf("zotcore: no agent")
	}
	r.mu.Lock()
	if r.activeCancel != nil {
		r.mu.Unlock()
		return CompactResult{}, ErrBusy
	}
	subCtx, cancel := context.WithCancel(ctx)
	r.activeCancel = cancel
	r.mu.Unlock()
	defer func() {
		r.mu.Lock()
		r.activeCancel = nil
		r.mu.Unlock()
	}()

	summary, err := r.agent.Compact(subCtx, 4, nil)
	if err != nil {
		return CompactResult{}, err
	}
	return CompactResult{Summary: summary, Messages: r.Messages()}, nil
}

// SetModel switches the active model. Same provider only; for cross-
// provider switches, create a new Runtime.
func (r *Runtime) SetModel(model string) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	if r.agent == nil {
		return fmt.Errorf("zotcore: no agent")
	}
	if _, err := provider.FindModel(r.provider, model); err != nil {
		return err
	}
	r.agent.Model = model
	r.model = model
	return nil
}

// State returns a snapshot of the runtime's current state. Useful for
// driving UIs.
func (r *Runtime) State() State {
	r.mu.Lock()
	defer r.mu.Unlock()
	return State{
		Provider:     r.provider,
		Model:        r.model,
		CWD:          r.cwd,
		Busy:         r.activeCancel != nil,
		MessageCount: len(r.Messages()),
	}
}

// Close releases resources held by the Runtime. Cancels any active
// prompt. Subsequent calls return ErrClosed.
func (r *Runtime) Close() error {
	r.mu.Lock()
	if r.closed {
		r.mu.Unlock()
		return nil
	}
	r.closed = true
	cancel := r.activeCancel
	r.mu.Unlock()
	if cancel != nil {
		cancel()
	}
	return nil
}

// ListModels returns every model known to the runtime for the
// current provider (catalog + live discovery if cached).
func (r *Runtime) ListModels() []ModelInfo {
	models := provider.ModelsForProvider(r.Provider())
	out := make([]ModelInfo, 0, len(models))
	for _, m := range models {
		out = append(out, ModelInfo{
			ID:            m.ID,
			Provider:      m.Provider,
			ContextWindow: m.ContextWindow,
			MaxOutput:     m.MaxOutput,
			Reasoning:     m.Reasoning,
		})
	}
	return out
}

// ---- public errors ----

// ErrBusy is returned when a Prompt or Compact is started while
// another is in flight on the same Runtime.
var ErrBusy = errors.New("zotcore: runtime is busy")

// ErrClosed is returned by methods on a Runtime after Close.
var ErrClosed = errors.New("zotcore: runtime is closed")
