// Package handlers — resolve.go
//
// v0.27.2: Extracted provider resolution and tool definition building
// from CompletionHandler methods to standalone functions. Both the HTTP
// completion handler and the headless task scheduler need these paths.
//
// The CompletionHandler methods (resolveConfig, buildToolDefs) remain as
// thin wrappers for backward compat — existing callers are unchanged.
//
// v0.29.0-cs7a: Replaced raw SQL with store methods.
package handlers

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"log"

	"chat-switchboard/crypto"
	"chat-switchboard/providers"
	"chat-switchboard/store"
	"chat-switchboard/tools"
)

// ── Provider Resolution ────────────────────────

// ProviderResolution holds the resolved provider configuration and metadata.
type ProviderResolution struct {
	Config        providers.ProviderConfig
	ProviderID    string // e.g. "anthropic", "openai"
	Model         string // resolved model ID
	ConfigID      string // provider_configs.id
	ProviderScope string // "personal", "team", "global"
}

// ResolveProviderConfig resolves the provider configuration for a completion
// request. Resolution order:
//  1. Explicit providerConfigID (from request or task definition)
//  2. Channel's configured provider (provider_config_id on channels table)
//  3. User's first active config (personal first, then global)
//
// The vault is used to decrypt API keys. Pass nil for unencrypted fallback.
func ResolveProviderConfig(
	stores store.Stores,
	vault *crypto.KeyResolver,
	userID, channelID, providerConfigID, modelID string,
) (ProviderResolution, error) {
	ctx := context.Background()
	configID := providerConfigID

	// 2. Config from channel
	if configID == "" && channelID != "" {
		channelConfigID, err := stores.Channels.GetProviderConfigID(ctx, channelID)
		if err == nil && channelConfigID != nil {
			configID = *channelConfigID
		}
	}

	// 3. User's first active config (personal first, then global — excludes team providers)
	if configID == "" {
		var err error
		configID, err = stores.Providers.FindFirstForUser(ctx, userID)
		if err != nil {
			return ProviderResolution{}, fmt.Errorf("no API config found — add one at /api-configs")
		}
	}

	// Load the config — allow personal, global, OR team configs the user belongs to
	cfg, err := stores.Providers.LoadAccessible(ctx, configID, userID)
	if err == sql.ErrNoRows {
		return ProviderResolution{}, fmt.Errorf("API config not found or not accessible")
	}
	if err != nil {
		return ProviderResolution{}, fmt.Errorf("failed to load API config: %w", err)
	}

	// Resolve model: explicit > config default
	model := modelID
	if model == "" && cfg.ModelDefault != "" {
		model = cfg.ModelDefault
	}
	if model == "" {
		return ProviderResolution{}, fmt.Errorf("no model specified and no default model in config")
	}

	// Decrypt API key using the appropriate tier
	key := ""
	if len(cfg.APIKeyEnc) > 0 {
		if vault != nil {
			var err error
			key, err = vault.Decrypt(cfg.APIKeyEnc, cfg.KeyNonce, cfg.KeyScope, userID)
			if err != nil {
				if err == crypto.ErrVaultLocked {
					return ProviderResolution{}, fmt.Errorf("personal vault is locked — please log in again")
				}
				return ProviderResolution{}, fmt.Errorf("failed to decrypt API key: %w", err)
			}
		} else {
			// No vault — key stored as raw bytes (unencrypted fallback)
			key = string(cfg.APIKeyEnc)
		}
	}

	// Parse custom headers
	customHeaders := make(map[string]string)
	if cfg.Headers != nil {
		b, _ := json.Marshal(cfg.Headers)
		_ = json.Unmarshal(b, &customHeaders)
	}

	// Parse provider-specific settings
	providerSettings := make(map[string]interface{})
	if cfg.Settings != nil {
		for k, v := range cfg.Settings {
			providerSettings[k] = v
		}
	}

	proxyMode := cfg.ProxyMode
	if proxyMode == "" {
		proxyMode = "system"
	}
	proxyURLStr := ""
	if cfg.ProxyURL != nil {
		proxyURLStr = *cfg.ProxyURL
	}

	return ProviderResolution{
		Config: providers.ProviderConfig{
			Endpoint:      cfg.Endpoint,
			APIKey:        key,
			CustomHeaders: customHeaders,
			Settings:      providerSettings,
			ProxyMode:     proxyMode,
			ProxyURL:      proxyURLStr,
		},
		ProviderID:    cfg.Provider,
		Model:         model,
		ConfigID:      cfg.ID,
		ProviderScope: cfg.Scope,
	}, nil
}

// ── Tool Definition Building ───────────────────

// BuildToolDefs assembles the tool definitions for a completion request.
// Includes server-registered tools filtered by context predicates,
// browser extension tools (if includeBrowser), and persona tool grant
// allowlisting.
//
// Additional tool grant filtering (e.g. task-level grants) can be applied
// by the caller after this function returns.
func BuildToolDefs(
	ctx context.Context,
	stores store.Stores,
	userID string,
	includeBrowser bool,
	disabledTools []string,
	tctx tools.ToolContext,
	personaID string,
) []providers.ToolDef {
	// Build disabled set for O(1) lookup
	disabled := make(map[string]bool, len(disabledTools))
	for _, name := range disabledTools {
		disabled[name] = true
	}

	// v0.25.0: Tools self-declare availability via predicates.
	allTools := tools.AvailableFor(tctx, disabled)
	defs := make([]providers.ToolDef, 0, len(allTools))
	for _, t := range allTools {
		defs = append(defs, providers.ToolDef{
			Type: "function",
			Function: providers.FunctionDef{
				Name:        t.Name,
				Description: t.Description,
				Parameters:  t.Parameters,
			},
		})
	}

	// Append browser-defined and starlark-tier tool schemas from extensions.
	// v0.29.2: starlark tools are always included (not gated on browser connection).
	if stores.Packages != nil {
		pkgs, err := stores.Packages.ListForUser(ctx, userID)
		if err != nil {
			log.Printf("⚠️ Failed to load extensions for tools: %v", err)
			return defs
		}
		for _, pkg := range pkgs {
			// Browser tools only when a browser client is connected.
			if pkg.Tier == "browser" && !includeBrowser {
				continue
			}
			// Only browser and starlark tiers expose tools this way.
			if pkg.Tier != "browser" && pkg.Tier != "starlark" {
				continue
			}
			// Starlark packages must be active to expose tools.
			if pkg.Tier == "starlark" && pkg.Status != "active" {
				continue
			}
			var manifest struct {
				Tools []struct {
					Name        string          `json:"name"`
					Description string          `json:"description"`
					Parameters  json.RawMessage `json:"parameters"`
				} `json:"tools"`
			}
			if err := json.Unmarshal(marshalManifest(pkg.Manifest), &manifest); err != nil {
				continue
			}
			for _, t := range manifest.Tools {
				if disabled[t.Name] {
					continue
				}
				defs = append(defs, providers.ToolDef{
					Type: "function",
					Function: providers.FunctionDef{
						Name:        t.Name,
						Description: t.Description,
						Parameters:  t.Parameters,
					},
				})
			}
		}
	}

	// v0.25.0: Persona tool grants — second-pass allowlist.
	// If the active persona has explicit tool grants, restrict to only those tools.
	// Empty grants = persona inherits all context-available tools (backward compat).
	if personaID != "" && stores.Personas != nil {
		grants, err := stores.Personas.GetToolGrants(ctx, personaID)
		if err != nil {
			log.Printf("⚠️ Failed to load tool grants for persona %s: %v", personaID, err)
		} else if len(grants) > 0 {
			allowed := make(map[string]bool, len(grants))
			for _, g := range grants {
				allowed[g] = true
			}
			filtered := defs[:0]
			for _, d := range defs {
				if allowed[d.Function.Name] {
					filtered = append(filtered, d)
				}
			}
			defs = filtered
		}
	}

	return defs
}

// BuildExtToolMap returns a map of toolName → PackageRegistration for all
// active starlark-tier extensions that declare tools in their manifest.
// Used by the tool loop to dispatch matched calls to on_tool_call.
func BuildExtToolMap(ctx context.Context, stores store.Stores, userID string) map[string]*store.PackageRegistration {
	if stores.Packages == nil {
		return nil
	}
	pkgs, err := stores.Packages.ListForUser(ctx, userID)
	if err != nil {
		return nil
	}
	result := make(map[string]*store.PackageRegistration)
	for i, pkg := range pkgs {
		if pkg.Tier != "starlark" || pkg.Status != "active" {
			continue
		}
		var manifest struct {
			Tools []struct {
				Name string `json:"name"`
			} `json:"tools"`
		}
		if json.Unmarshal(marshalManifest(pkg.Manifest), &manifest) != nil {
			continue
		}
		for _, t := range manifest.Tools {
			if t.Name != "" {
				result[t.Name] = &pkgs[i].PackageRegistration
			}
		}
	}
	return result
}

// FilterToolDefsByGrants applies an additional allowlist to tool defs.
// Used by the task scheduler to enforce task-level tool grants on top
// of persona-level grants. Passing nil or empty grants returns defs unchanged.
func FilterToolDefsByGrants(defs []providers.ToolDef, grants []string) []providers.ToolDef {
	if len(grants) == 0 {
		return defs
	}
	allowed := make(map[string]bool, len(grants))
	for _, g := range grants {
		allowed[g] = true
	}
	filtered := make([]providers.ToolDef, 0, len(defs))
	for _, d := range defs {
		if allowed[d.Function.Name] {
			filtered = append(filtered, d)
		}
	}
	return filtered
}
