package storage

import (
	"encoding/json"
	"errors"
	"github.com/smart-mcp-proxy/mcpproxy-go/internal/config"
	"time"
)

// ErrToolApprovalNotFound is returned by GetToolApproval when no record
// exists for the requested server+tool key. Callers that want to distinguish
// "first time we've seen this tool" from a real read error (corrupt JSON,
// closed DB, mmap remap during compaction) MUST use errors.Is, not a generic
// `err != nil` check — a transient decode error must not be misread as
// "missing", which would otherwise silently overwrite a pending/changed
// record with a synthesized approved one.
var ErrToolApprovalNotFound = errors.New("tool approval not found")

// Bucket names for bbolt database
const (
	UpstreamsBucket       = "upstreams"
	ToolStatsBucket       = "toolstats"
	ToolHashBucket        = "toolhash"
	ToolApprovalBucket    = "tool_approvals"
	OAuthTokenBucket      = "oauth_tokens" //nolint:gosec // bucket name, not a credential
	OAuthCompletionBucket = "oauth_completion"
	MetaBucket            = "meta"
	CacheBucket           = "cache"
	CacheStatsBucket      = "cache_stats"
	SessionsBucket        = "sessions"

	// Security scanner buckets (Spec 039)
	ScannersBucket           = "security_scanners"
	ScanJobsBucket           = "security_scan_jobs"
	ScanJobIndexBucket       = "security_scan_job_index" // lightweight ScanJobMeta index (MCP-2205)
	ScanReportsBucket        = "security_reports"
	IntegrityBaselinesBucket = "integrity_baselines"

	// Onboarding wizard bucket (Spec 046)
	OnboardingBucket = "onboarding"
)

// Onboarding state keys (Spec 046)
const (
	OnboardingStateKey = "wizard_state"
)

// Onboarding step-status values (Spec 046; enum widened by Spec 080 FR-001).
const (
	// StepStatusCompleted means the user completed the step inside the wizard.
	StepStatusCompleted = "completed"
	// StepStatusCompletedExternal means the wizard's connect step was never
	// advanced, but at dismissal time the install showed positive evidence of
	// a connection made outside the wizard (a supported client currently
	// connected, or an MCP client has ever handshaked). Connect step only
	// (Spec 080 FR-002).
	StepStatusCompletedExternal = "completed_external"
	// StepStatusSkipped means the wizard was dismissed with the step
	// untouched and no external-connection evidence was established.
	StepStatusSkipped = "skipped"
)

// OnboardingState records whether the user has engaged with the first-run
// wizard, and which steps they completed or skipped. Persisted under
// OnboardingBucket / OnboardingStateKey. Absence of the record means
// "wizard has never been shown to this installation".
type OnboardingState struct {
	// Engaged is true once the wizard was shown and the user completed or
	// skipped it. Once true, the wizard does not auto-show again, even if
	// state regresses (e.g. user disconnects all clients).
	Engaged bool `json:"engaged"`

	// FirstShownAt is the timestamp of first wizard render.
	FirstShownAt *time.Time `json:"first_shown_at,omitempty"`

	// EngagedAt is the timestamp of completion or explicit skip.
	EngagedAt *time.Time `json:"engaged_at,omitempty"`

	// ConnectStepStatus is one of: "", "completed", "completed_external",
	// "skipped" (Spec 080 FR-001). "completed_external" records a dismissal
	// where the connect step was untouched but the install was already
	// connected outside the wizard (CLI, ConnectModal, manual config).
	ConnectStepStatus string `json:"connect_step_status,omitempty"`

	// ServerStepStatus is one of: "", "completed", "skipped".
	ServerStepStatus string `json:"server_step_status,omitempty"`
}

// Meta keys
const (
	SchemaVersionKey       = "schema"
	DockerRecoveryStateKey = "docker_recovery_state"
)

// Current schema version
const CurrentSchemaVersion = 3

// OutputSchemaHashSchemaVersion is the schema version that starts including
// MCP outputSchema in the tool approval hash baseline.
const OutputSchemaHashSchemaVersion = 3

// UpstreamRecord represents an upstream server record in storage
type UpstreamRecord struct {
	ID             string                  `json:"id"`
	Name           string                  `json:"name"`
	URL            string                  `json:"url,omitempty"`
	Protocol       string                  `json:"protocol,omitempty"` // stdio, http, sse, streamable-http, auto
	Command        string                  `json:"command,omitempty"`
	Args           []string                `json:"args,omitempty"`
	WorkingDir     string                  `json:"working_dir,omitempty"` // Working directory for stdio servers
	Env            map[string]string       `json:"env,omitempty"`
	Headers        map[string]string       `json:"headers,omitempty"` // For HTTP authentication
	OAuth          *config.OAuthConfig     `json:"oauth,omitempty"`   // OAuth configuration
	Enabled        bool                    `json:"enabled"`
	Quarantined    bool                    `json:"quarantined"` // Security quarantine status
	Created        time.Time               `json:"created"`
	Updated        time.Time               `json:"updated"`
	Isolation      *config.IsolationConfig `json:"isolation,omitempty"`        // Per-server isolation settings
	ReconnectOnUse bool                    `json:"reconnect_on_use,omitempty"` // Attempt reconnection on tool call
	// AutoApproveToolChanges (MCP-2930/MCP-2940) is the per-server intent to
	// auto-approve new/changed tools past the trust baseline. Tri-state *bool.
	// Persisted to BBolt because SaveConfiguration rebuilds the JSON config's
	// server list from these records — a field absent here is wiped on the
	// next mutation, so REST/UI toggling (MCP-2932) and runtime enforcement
	// (MCP-2931) would not survive a save/restart without it.
	AutoApproveToolChanges *bool           `json:"auto_approve_tool_changes,omitempty"`
	LauncherWaitTimeout    config.Duration `json:"launcher_wait_timeout,omitempty"` // Spec 046: max wait for locally-launched HTTP/SSE upstream URL to become reachable
	EnabledTools           []string        `json:"enabled_tools,omitempty"`         // Allowlist: only these tools are exposed
	DisabledTools          []string        `json:"disabled_tools,omitempty"`        // Denylist: these tools are hidden
	// MCP-866: persist a server's registry origin + provenance so the
	// approval/quarantine view and the custom-origin skip_quarantine guard
	// survive a restart.
	SourceRegistryID         string `json:"source_registry_id,omitempty"`
	SourceRegistryProvenance string `json:"source_registry_provenance,omitempty"`
	// Spec 074: per-server discovery/health-check interval overrides, persisted
	// so REST-API/UI-set overrides survive a restart. *Duration tri-state:
	// nil = inherit, pointer-to-0s = disabled, positive = interval.
	HealthCheckInterval   *config.Duration `json:"health_check_interval,omitempty"`
	ToolDiscoveryInterval *config.Duration `json:"tool_discovery_interval,omitempty"`
	// MCP-3322: per-server MCP `initialize` handshake deadline override,
	// persisted so a REST/UI/CLI-set init_timeout survives a restart.
	InitTimeout *config.Duration `json:"init_timeout,omitempty"`
}

// ToolStatRecord represents tool usage statistics
type ToolStatRecord struct {
	ToolName string    `json:"tool_name"`
	Count    uint64    `json:"count"`
	LastUsed time.Time `json:"last_used"`
}

// ToolHashRecord represents a tool hash for change detection
type ToolHashRecord struct {
	ToolName string    `json:"tool_name"`
	Hash     string    `json:"hash"`
	Updated  time.Time `json:"updated"`
}

// ToolApprovalStatus constants for tool-level quarantine
const (
	ToolApprovalStatusApproved = "approved"
	ToolApprovalStatusPending  = "pending"
	ToolApprovalStatusChanged  = "changed"
)

// ToolApprovalRecord represents a tool's approval status for tool-level quarantine.
// When a tool is first discovered, it starts as "pending". Once approved, it becomes "approved".
// If the tool's description or schema changes after approval, it becomes "changed".
type ToolApprovalRecord struct {
	ServerName           string    `json:"server_name"`
	ToolName             string    `json:"tool_name"`
	ApprovedHash         string    `json:"approved_hash"`
	CurrentHash          string    `json:"current_hash"`
	HashSchemaVersion    uint64    `json:"hash_schema_version,omitempty"`
	Status               string    `json:"status"` // "approved", "pending", "changed"
	ApprovedAt           time.Time `json:"approved_at"`
	ApprovedBy           string    `json:"approved_by"`
	PreviousDescription  string    `json:"previous_description,omitempty"`
	CurrentDescription   string    `json:"current_description,omitempty"`
	PreviousSchema       string    `json:"previous_schema,omitempty"`
	CurrentSchema        string    `json:"current_schema,omitempty"`
	PreviousOutputSchema string    `json:"previous_output_schema,omitempty"`
	CurrentOutputSchema  string    `json:"current_output_schema,omitempty"`
	Disabled             bool      `json:"disabled,omitempty"`
}

// ToolApprovalKey returns the storage key for a tool approval record.
func ToolApprovalKey(serverName, toolName string) string {
	return serverName + ":" + toolName
}

// Key returns the storage key for this tool approval record.
func (r *ToolApprovalRecord) Key() string {
	return ToolApprovalKey(r.ServerName, r.ToolName)
}

// MarshalBinary implements encoding.BinaryMarshaler
func (r *ToolApprovalRecord) MarshalBinary() ([]byte, error) {
	return json.Marshal(r)
}

// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (r *ToolApprovalRecord) UnmarshalBinary(data []byte) error {
	return json.Unmarshal(data, r)
}

// OAuthTokenRecord represents stored OAuth tokens for a server
type OAuthTokenRecord struct {
	ServerName   string    `json:"server_name"`            // Storage key (serverName_hash format)
	DisplayName  string    `json:"display_name,omitempty"` // Actual server name (for RefreshManager lookup)
	AccessToken  string    `json:"access_token"`
	RefreshToken string    `json:"refresh_token,omitempty"`
	TokenType    string    `json:"token_type"`
	ExpiresAt    time.Time `json:"expires_at"`
	Scopes       []string  `json:"scopes,omitempty"`
	Created      time.Time `json:"created"`
	Updated      time.Time `json:"updated"`
	// ClientID and ClientSecret are persisted for DCR (Dynamic Client Registration)
	// These are required for token refresh when using DCR-obtained credentials
	ClientID     string `json:"client_id,omitempty"`
	ClientSecret string `json:"client_secret,omitempty"`
	// CallbackPort and RedirectURI are persisted for OAuth redirect URI port persistence (Spec 022)
	// These ensure re-authentication uses the same port registered during DCR
	CallbackPort int    `json:"callback_port,omitempty"` // Port used during DCR for redirect_uri
	RedirectURI  string `json:"redirect_uri,omitempty"`  // Full redirect URI registered with DCR
}

// GetServerName returns the actual server name for RefreshManager lookup.
// Falls back to ServerName if DisplayName is not set (for backward compatibility).
func (r *OAuthTokenRecord) GetServerName() string {
	if r.DisplayName != "" {
		return r.DisplayName
	}
	return r.ServerName
}

// OAuthCompletionEvent represents an OAuth completion event for cross-process notification
type OAuthCompletionEvent struct {
	ServerName  string     `json:"server_name"`
	CompletedAt time.Time  `json:"completed_at"`
	ProcessedAt *time.Time `json:"processed_at,omitempty"` // Nil if not yet processed by server
}

// DockerRecoveryState represents the persistent state of Docker recovery
// This allows recovery to resume after application restart
type DockerRecoveryState struct {
	LastAttempt      time.Time `json:"last_attempt"`
	FailureCount     int       `json:"failure_count"`
	DockerAvailable  bool      `json:"docker_available"`
	RecoveryMode     bool      `json:"recovery_mode"`
	LastError        string    `json:"last_error,omitempty"`
	AttemptsSinceUp  int       `json:"attempts_since_up"` // Attempts since Docker was last available
	LastSuccessfulAt time.Time `json:"last_successful_at,omitempty"`
}

// MarshalBinary implements encoding.BinaryMarshaler
func (u *UpstreamRecord) MarshalBinary() ([]byte, error) {
	return json.Marshal(u)
}

// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (u *UpstreamRecord) UnmarshalBinary(data []byte) error {
	return json.Unmarshal(data, u)
}

// MarshalBinary implements encoding.BinaryMarshaler
func (t *ToolStatRecord) MarshalBinary() ([]byte, error) {
	return json.Marshal(t)
}

// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (t *ToolStatRecord) UnmarshalBinary(data []byte) error {
	return json.Unmarshal(data, t)
}

// MarshalBinary implements encoding.BinaryMarshaler
func (h *ToolHashRecord) MarshalBinary() ([]byte, error) {
	return json.Marshal(h)
}

// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (h *ToolHashRecord) UnmarshalBinary(data []byte) error {
	return json.Unmarshal(data, h)
}

// MarshalBinary implements encoding.BinaryMarshaler
func (o *OAuthTokenRecord) MarshalBinary() ([]byte, error) {
	return json.Marshal(o)
}

// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (o *OAuthTokenRecord) UnmarshalBinary(data []byte) error {
	return json.Unmarshal(data, o)
}

// MarshalBinary implements encoding.BinaryMarshaler
func (e *OAuthCompletionEvent) MarshalBinary() ([]byte, error) {
	return json.Marshal(e)
}

// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (e *OAuthCompletionEvent) UnmarshalBinary(data []byte) error {
	return json.Unmarshal(data, e)
}

// MarshalBinary implements encoding.BinaryMarshaler
func (d *DockerRecoveryState) MarshalBinary() ([]byte, error) {
	return json.Marshal(d)
}

// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (d *DockerRecoveryState) UnmarshalBinary(data []byte) error {
	return json.Unmarshal(data, d)
}
