/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *--------------------------------------------------------------------------------------------*/

package copilot

import (
	"context"
	"errors"
	"fmt"

	"github.com/github/copilot-sdk/go/rpc"
)

// validateNewClientForMode checks the cross-cutting requirements that
// [ModeEmpty] places on [ClientOptions]. Called from [NewClient].
func validateNewClientForMode(opts *ClientOptions) {
	if opts == nil || opts.Mode != ModeEmpty {
		return
	}
	// Empty mode requires durable, app-owned storage. Either:
	//   - the app supplied a BaseDirectory the runtime can write to,
	//   - the app supplied a SessionFS implementation,
	//   - or the app is connecting to an externally-managed runtime via
	//     URIConnection (in which case the host owns storage).
	if opts.BaseDirectory != "" {
		return
	}
	if opts.SessionFS != nil {
		return
	}
	if _, ok := opts.Connection.(URIConnection); ok {
		return
	}
	panic("Client is in Mode=ModeEmpty but neither BaseDirectory, SessionFS, nor a URIConnection was supplied. " +
		"Empty mode requires explicit, per-tenant storage; set ClientOptions.BaseDirectory or .SessionFS, " +
		"or connect to an externally-managed runtime via URIConnection.")
}

// validateToolFilterList rejects bare "*" entries with an actionable error
// pointing at the [ToolSet] builder. Called for both availableTools and
// excludedTools.
func validateToolFilterList(field string, list []string) error {
	for _, entry := range list {
		if entry == "*" {
			return fmt.Errorf(
				"invalid %s entry %q: there is no bare wildcard. "+
					"Use one or more of NewToolSet().AddBuiltIn(\"*\"), .AddMCP(\"*\"), or .AddCustom(\"*\") "+
					"to target a specific source",
				field, entry)
		}
	}
	return nil
}

// resolveToolFilterOptions validates the configured tool filters and applies
// empty-mode invariants. Returns the (possibly-mutated) request fields to set.
func (c *Client) resolveToolFilterOptions(availableTools, excludedTools []string) (
	[]string, []string, *rpc.OptionsUpdateToolFilterPrecedence, error,
) {
	if err := validateToolFilterList("availableTools", availableTools); err != nil {
		return nil, nil, nil, err
	}
	if err := validateToolFilterList("excludedTools", excludedTools); err != nil {
		return nil, nil, nil, err
	}
	if c.options.Mode == ModeEmpty && availableTools == nil {
		return nil, nil, nil, errors.New(
			"Client is in Mode=ModeEmpty but the session config did not specify AvailableTools. " +
				"Empty mode requires every session to explicitly opt into the tools it wants — " +
				"e.g. NewToolSet().AddBuiltIn(BuiltInToolsIsolated...).ToSlice()")
	}
	precedence := rpc.OptionsUpdateToolFilterPrecedenceExcluded
	return availableTools, excludedTools, &precedence, nil
}

// systemMessageForMode applies empty-mode environment_context stripping to
// the caller-supplied system message config. App values win (we only inject
// when the app hasn't already specified an environment_context override).
func (c *Client) systemMessageForMode(supplied *SystemMessageConfig) *SystemMessageConfig {
	if c.options.Mode != ModeEmpty {
		return supplied
	}
	removeAction := SectionOverride{Action: SectionActionRemove}
	if supplied == nil {
		return &SystemMessageConfig{
			Mode:     "customize",
			Sections: map[string]SectionOverride{"environment_context": removeAction},
		}
	}
	switch supplied.Mode {
	case "replace":
		return supplied
	case "customize":
		if _, ok := supplied.Sections["environment_context"]; ok {
			return supplied
		}
		out := *supplied
		out.Sections = make(map[string]SectionOverride, len(supplied.Sections)+1)
		for k, v := range supplied.Sections {
			out.Sections[k] = v
		}
		out.Sections["environment_context"] = removeAction
		return &out
	case "append", "":
		// Promote append/unspecified to customize so we can also strip
		// environment_context. The runtime appends Content to additional
		// instructions in both modes, so caller text is preserved verbatim.
		return &SystemMessageConfig{
			Mode:     "customize",
			Content:  supplied.Content,
			Sections: map[string]SectionOverride{"environment_context": removeAction},
		}
	default:
		return supplied
	}
}

// applyConfigDefaultsForMode fills in empty-mode defaults on the session
// config in place. App-supplied values win.
func (c *Client) applyConfigDefaultsForMode(config *SessionConfig) {
	if c.options.Mode != ModeEmpty {
		return
	}
	if config.EnableExperimentalMode == nil {
		f := false
		config.EnableExperimentalMode = &f
	}
	if config.EnableSessionTelemetry == nil {
		f := false
		config.EnableSessionTelemetry = &f
	}
	if config.SkipEmbeddingRetrieval == nil {
		t := true
		config.SkipEmbeddingRetrieval = &t
	}
	if config.EmbeddingCacheStorage == nil {
		inMemory := "in-memory"
		config.EmbeddingCacheStorage = &inMemory
	}
	if config.EnableOnDemandInstructionDiscovery == nil {
		f := false
		config.EnableOnDemandInstructionDiscovery = &f
	}
	if config.EnableFileHooks == nil {
		f := false
		config.EnableFileHooks = &f
	}
	if config.EnableHostGitOperations == nil {
		f := false
		config.EnableHostGitOperations = &f
	}
	if config.EnableSessionStore == nil {
		f := false
		config.EnableSessionStore = &f
	}
	if config.EnableSkills == nil {
		f := false
		config.EnableSkills = &f
	}
	if config.Memory == nil {
		config.Memory = &MemoryConfiguration{Enabled: false}
	}
	if config.MCPOAuthTokenStorage == "" {
		config.MCPOAuthTokenStorage = "in-memory"
	}
	if config.CustomAgentsLocalOnly == nil {
		localOnly := true
		config.CustomAgentsLocalOnly = &localOnly
	}
}

func (c *Client) applyResumeDefaultsForMode(config *ResumeSessionConfig) {
	if c.options.Mode != ModeEmpty {
		return
	}
	if config.EnableExperimentalMode == nil {
		f := false
		config.EnableExperimentalMode = &f
	}
	if config.EnableSessionTelemetry == nil {
		f := false
		config.EnableSessionTelemetry = &f
	}
	if config.SkipEmbeddingRetrieval == nil {
		t := true
		config.SkipEmbeddingRetrieval = &t
	}
	if config.EmbeddingCacheStorage == nil {
		inMemory := "in-memory"
		config.EmbeddingCacheStorage = &inMemory
	}
	if config.EnableOnDemandInstructionDiscovery == nil {
		f := false
		config.EnableOnDemandInstructionDiscovery = &f
	}
	if config.EnableFileHooks == nil {
		f := false
		config.EnableFileHooks = &f
	}
	if config.EnableHostGitOperations == nil {
		f := false
		config.EnableHostGitOperations = &f
	}
	if config.EnableSessionStore == nil {
		f := false
		config.EnableSessionStore = &f
	}
	if config.EnableSkills == nil {
		f := false
		config.EnableSkills = &f
	}
	if config.Memory == nil {
		config.Memory = &MemoryConfiguration{Enabled: false}
	}
	if config.MCPOAuthTokenStorage == "" {
		config.MCPOAuthTokenStorage = "in-memory"
	}
	if config.CustomAgentsLocalOnly == nil {
		localOnly := true
		config.CustomAgentsLocalOnly = &localOnly
	}
}

// updateSessionOptionsForMode applies the per-mode safe-defaults patch via
// session.options.update after create/resume succeeds. In empty mode the
// four overridable feature flags default to safe values; caller values win.
// installedPlugins=[] is unconditional in empty mode. IncludedBuiltinSkills
// defaults to [] but callers can explicitly allow selected runtime-bundled skills.
func (c *Client) updateSessionOptionsForMode(ctx context.Context, session *Session, base optBackInFields) error {
	patch := &rpc.SessionUpdateOptionsParams{}
	hasAny := false
	if c.options.Mode == ModeEmpty {
		if base.SkipCustomInstructions != nil {
			patch.SkipCustomInstructions = base.SkipCustomInstructions
		} else {
			t := true
			patch.SkipCustomInstructions = &t
		}
		if base.CustomAgentsLocalOnly != nil {
			patch.CustomAgentsLocalOnly = base.CustomAgentsLocalOnly
		} else {
			t := true
			patch.CustomAgentsLocalOnly = &t
		}
		if base.CoauthorEnabled != nil {
			patch.CoauthorEnabled = base.CoauthorEnabled
		} else {
			f := false
			patch.CoauthorEnabled = &f
		}
		if base.ManageScheduleEnabled != nil {
			patch.ManageScheduleEnabled = base.ManageScheduleEnabled
		} else {
			f := false
			patch.ManageScheduleEnabled = &f
		}
		patch.InstalledPlugins = []rpc.SessionInstalledPlugin{}
		if base.IncludedBuiltinSkills != nil {
			patch.IncludedBuiltinSkills = base.IncludedBuiltinSkills
		} else {
			patch.IncludedBuiltinSkills = []string{}
		}
		hasAny = true
	} else {
		if base.SkipCustomInstructions != nil {
			patch.SkipCustomInstructions = base.SkipCustomInstructions
			hasAny = true
		}
		if base.CustomAgentsLocalOnly != nil {
			patch.CustomAgentsLocalOnly = base.CustomAgentsLocalOnly
			hasAny = true
		}
		if base.CoauthorEnabled != nil {
			patch.CoauthorEnabled = base.CoauthorEnabled
			hasAny = true
		}
		if base.ManageScheduleEnabled != nil {
			patch.ManageScheduleEnabled = base.ManageScheduleEnabled
			hasAny = true
		}
		if base.IncludedBuiltinSkills != nil {
			patch.IncludedBuiltinSkills = base.IncludedBuiltinSkills
			hasAny = true
		}
	}
	if !hasAny {
		return nil
	}
	if _, err := session.RPC.Options.Update(ctx, patch); err != nil {
		// The runtime session exists but the post-create options patch
		// failed — best-effort disconnect so we don't leak it (in empty
		// mode it would otherwise keep running with permissive defaults).
		_ = session.Disconnect()
		c.sessionsMux.Lock()
		delete(c.sessions, session.SessionID)
		c.sessionsMux.Unlock()
		return fmt.Errorf("failed to apply mode-specific session options: %w", err)
	}
	return nil
}

// optBackInFields is the subset of SessionConfig / ResumeSessionConfig shared
// by [Client.updateSessionOptionsForMode].
type optBackInFields struct {
	SkipCustomInstructions *bool
	CustomAgentsLocalOnly  *bool
	CoauthorEnabled        *bool
	ManageScheduleEnabled  *bool
	IncludedBuiltinSkills  []string
}