package webagent

import (
	"context"
	"strings"
	"sync"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/daemon"
	"github.com/tidbcloud/AgentTeam/internal/runtimecontrol"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/app/runtime"
	"github.com/tidbcloud/AgentTeam/internal/store/organization"
	"github.com/tidbcloud/AgentTeam/internal/store/rewind"
)

type fakeRuntime struct {
	mu sync.Mutex

	startCalls        []agentCall
	startErr          error
	restartCalls      []agentCall
	restartErr        error
	connectedMachines map[string]bool
	runningAgents     map[agentCall]bool
	runningResults    []bool
	sessionIDs        map[agentCall]string
	runtimeStatuses   map[agentCall]agentRuntimeStatus
	permissionModeOK  *bool
	maasDirectOK      *bool
	maasDirectKnown   bool

	maasDirectByMachine    map[string]*bool
	maasDirectMachineCalls []string
	runtimeRuntimes        map[string][]string
	runtimeDefaults        map[string]map[string]runtime.RuntimeDefaults
	catalogMachineCalls    []string

	stopCalls         []agentCall
	stopErr           error
	resetSessionCalls []agentCall
	resetSessionErr   error
	identityCalls     []identityCall
	identityErr       error
	resetCalls        []agentCall
	resetErr          error
	gcCalls           []gcCall
	gcErr             error
	syncMachineCalls  []string
	syncMachineErr    error
	notifyMemoryCalls []string
	notifyMemoryErr   error

	workspaceFiles    []agent.WorkspaceFile
	workspacePage     *agent.WorkspaceFilePage
	workspaceErr      error
	lastWorkspaceDir  string
	lastWorkspaceOpts agent.WorkspaceListOptions
	workspaceContent  agent.WorkspaceFileContent
	workspaceReadErr  error
	lastWorkspaceRead string
	memorySyncStatus  daemon.AgentMemorySyncStatus
	memorySyncKnown   bool
	memorySyncErr     error
	memoryRetryOK     bool
	memoryRetryErr    error
	lastMemorySync    agentCall

	outputFiles     []agent.WorkspaceFile
	outputFilesPage *agent.WorkspaceFilePage
	outputFilesErr  error
	lastOutputFiles outputFilesCall
	lastOutputOpts  agent.WorkspaceListOptions
	outputContent   agent.WorkspaceFileContent
	outputReadErr   error
	lastOutputRead  outputReadCall

	validateErr    error
	lastValidate   validateCall
	summary        agent.AgentReviewSummary
	summaryErr     error
	lastSummary    reviewCall
	snapshot       *agent.AgentReviewSnapshot
	snapshotErr    error
	fingerprint    *agent.AgentReviewFingerprint
	fingerprintErr error
	reviewFiles    []agent.AgentReviewFile
	reviewFilesErr error
	lastFiles      reviewCall
	diff           agent.AgentReviewDiff
	diffErr        error
	diffBatchErr   error
	lastDiff       diffCall

	trajectoryItems []runtimecontrol.AgentTrajectoryItem
	trajectoryErr   error
	lastTrajectory  trajectoryCall

	lastRemoteRequest runtimecontrol.RemoteRequestContext
}

type fakeRuntimeCommands struct {
	startErr     error
	startAgent   agent.AgentView
	startAgentID string
}

func (fake *fakeRuntimeCommands) StartAgentRuntimeForUser(_ context.Context, _ string, _ string, agentID string) (agent.AgentView, error) {
	fake.startAgentID = agentID
	if fake.startErr != nil {
		return agent.AgentView{}, fake.startErr
	}
	if fake.startAgent.AgentID == "" {
		fake.startAgent = agent.AgentView{AgentID: agentID, MachineID: "mach_1"}
	}
	return fake.startAgent, nil
}

func (fake *fakeRuntimeCommands) StopAgentRuntimeForUser(context.Context, string, string, string) (agent.AgentView, error) {
	return agent.AgentView{}, nil
}

func (fake *fakeRuntimeCommands) ResetAgentRuntimeForUser(context.Context, string, string, string, bool) error {
	return nil
}

type agentCall struct{ MachineID, AgentID string }
type identityCall struct{ MachineID, AgentID, Name, DisplayName string }
type gcCall struct {
	MachineID, AgentID string
	Points             []rewind.MessageRecoveryPoint
}
type outputFilesCall struct{ MachineID, AgentID, TargetPath, DirPath string }
type outputReadCall struct{ MachineID, AgentID, TargetPath, Path string }
type validateCall struct{ MachineID, AgentID, Kind, TargetPath string }
type reviewCall struct{ MachineID, AgentID, TargetPath, Scope, BaseRef, HeadRef, ComparisonMode string }
type diffCall struct {
	reviewCall
	Path string
}
type trajectoryCall struct {
	MachineID, AgentID string
	Limit              int
	Before             int64
}

func (fake *fakeRuntime) StartAgent(_ context.Context, machineID, agentID string) error {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.startCalls = append(fake.startCalls, agentCall{machineID, agentID})
	return fake.startErr
}

func (fake *fakeRuntime) AgentRuntimeRunning(machineID, agentID string) bool {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	if len(fake.runningResults) > 0 {
		result := fake.runningResults[0]
		fake.runningResults = fake.runningResults[1:]
		return result
	}
	return fake.runningAgents[agentCall{machineID, agentID}]
}

func (fake *fakeRuntime) MachineConnected(machineID string) bool {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	return fake.connectedMachines[machineID]
}

func (fake *fakeRuntime) AgentRuntimeSessionID(machineID string, agentID string) (string, bool) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	sessionID := strings.TrimSpace(fake.sessionIDs[agentCall{machineID, agentID}])
	return sessionID, sessionID != ""
}

func (fake *fakeRuntime) AgentPermissionModeSupported(string) bool {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	if fake.permissionModeOK == nil {
		return true
	}
	return *fake.permissionModeOK
}

func (fake *fakeRuntime) AgentMaaSDirectSupport(machineID string) (bool, bool) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.maasDirectMachineCalls = append(fake.maasDirectMachineCalls, machineID)
	if supported, ok := fake.maasDirectByMachine[machineID]; ok {
		if supported == nil {
			return false, false
		}
		return *supported, true
	}
	if fake.maasDirectOK == nil {
		return false, fake.maasDirectKnown
	}
	return *fake.maasDirectOK, true
}

func (fake *fakeRuntime) MachineRuntimeCatalog(machineID string) ([]string, map[string]runtime.RuntimeDefaults, bool) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.catalogMachineCalls = append(fake.catalogMachineCalls, machineID)
	runtimes, ok := fake.runtimeRuntimes[machineID]
	if !ok {
		return nil, nil, false
	}
	return runtimes, fake.runtimeDefaults[machineID], true
}

func (fake *fakeRuntime) RestartAgent(_ context.Context, machineID, agentID string) error {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.restartCalls = append(fake.restartCalls, agentCall{machineID, agentID})
	return fake.restartErr
}

func (fake *fakeRuntime) StopAgent(machineID, agentID string) error {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.stopCalls = append(fake.stopCalls, agentCall{machineID, agentID})
	return fake.stopErr
}

func (fake *fakeRuntime) InterruptAgentRun(string, string) error {
	return nil
}

func (fake *fakeRuntime) ResetAgentSession(machineID, agentID string) error {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.resetSessionCalls = append(fake.resetSessionCalls, agentCall{machineID, agentID})
	return fake.resetSessionErr
}

func (fake *fakeRuntime) UpdateAgentIdentity(machineID, agentID, name, displayName string) error {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.identityCalls = append(fake.identityCalls, identityCall{machineID, agentID, name, displayName})
	return fake.identityErr
}

func (fake *fakeRuntime) SyncMachineCatalog(_ context.Context, machineID string) error {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.syncMachineCalls = append(fake.syncMachineCalls, machineID)
	return fake.syncMachineErr
}

func (fake *fakeRuntime) NotifyMemoryAccessChangedByAgent(_ context.Context, _ string, agentID string) error {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.notifyMemoryCalls = append(fake.notifyMemoryCalls, agentID)
	return fake.notifyMemoryErr
}

func (fake *fakeRuntime) ResetAgentWorkspace(machineID, agentID string) error {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.resetCalls = append(fake.resetCalls, agentCall{machineID, agentID})
	return fake.resetErr
}

func (fake *fakeRuntime) ListAgentWorkspace(_ context.Context, _ string, _ string, dirPath string) ([]agent.WorkspaceFile, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastWorkspaceDir = dirPath
	return fake.workspaceFiles, fake.workspaceErr
}

func (fake *fakeRuntime) ListAgentWorkspacePage(ctx context.Context, _ string, _ string, dirPath string, opts agent.WorkspaceListOptions) (agent.WorkspaceFilePage, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastRemoteRequest = runtimecontrol.RemoteRequestContextFromContext(ctx)
	fake.lastWorkspaceDir = dirPath
	fake.lastWorkspaceOpts = opts
	if fake.workspacePage != nil {
		return *fake.workspacePage, fake.workspaceErr
	}
	return agent.WorkspaceFilePage{
		Files:  fake.workspaceFiles,
		Limit:  opts.Limit,
		Cursor: opts.Cursor,
	}, fake.workspaceErr
}

func (fake *fakeRuntime) ReadAgentWorkspaceFile(ctx context.Context, _ string, _ string, path string) (agent.WorkspaceFileContent, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastRemoteRequest = runtimecontrol.RemoteRequestContextFromContext(ctx)
	fake.lastWorkspaceRead = path
	return fake.workspaceContent, fake.workspaceReadErr
}

func (fake *fakeRuntime) GetAgentMemorySyncStatus(_ context.Context, machineID, agentID string) (daemon.AgentMemorySyncStatus, bool, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastMemorySync = agentCall{machineID, agentID}
	return fake.memorySyncStatus, fake.memorySyncKnown, fake.memorySyncErr
}

func (fake *fakeRuntime) RetryAgentMemorySync(_ context.Context, machineID, agentID string) (bool, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastMemorySync = agentCall{machineID, agentID}
	return fake.memoryRetryOK, fake.memoryRetryErr
}

func (fake *fakeRuntime) ListAgentOutputTargetFiles(_ context.Context, machineID, agentID, targetPath, dirPath string) ([]agent.WorkspaceFile, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastOutputFiles = outputFilesCall{machineID, agentID, targetPath, dirPath}
	return fake.outputFiles, fake.outputFilesErr
}

func (fake *fakeRuntime) ListAgentOutputTargetFilesPage(ctx context.Context, machineID, agentID, targetPath, dirPath string, opts agent.WorkspaceListOptions) (agent.WorkspaceFilePage, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastRemoteRequest = runtimecontrol.RemoteRequestContextFromContext(ctx)
	fake.lastOutputFiles = outputFilesCall{machineID, agentID, targetPath, dirPath}
	fake.lastOutputOpts = opts
	if fake.outputFilesPage != nil {
		return *fake.outputFilesPage, fake.outputFilesErr
	}
	return agent.WorkspaceFilePage{
		Files:  fake.outputFiles,
		Limit:  opts.Limit,
		Cursor: opts.Cursor,
	}, fake.outputFilesErr
}

func (fake *fakeRuntime) ReadAgentOutputTargetFile(ctx context.Context, machineID, agentID, targetPath, path string) (agent.WorkspaceFileContent, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastRemoteRequest = runtimecontrol.RemoteRequestContextFromContext(ctx)
	fake.lastOutputRead = outputReadCall{machineID, agentID, targetPath, path}
	return fake.outputContent, fake.outputReadErr
}

func (fake *fakeRuntime) GetAgentReviewSummary(ctx context.Context, machineID, agentID, targetPath, baseRef, headRef, comparisonMode string) (agent.AgentReviewSummary, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastRemoteRequest = runtimecontrol.RemoteRequestContextFromContext(ctx)
	fake.lastSummary = reviewCall{MachineID: machineID, AgentID: agentID, TargetPath: targetPath, BaseRef: baseRef, HeadRef: headRef, ComparisonMode: comparisonMode}
	return fake.summary, fake.summaryErr
}

func (fake *fakeRuntime) GetAgentReviewSnapshot(ctx context.Context, machineID, agentID, targetPath, baseRef, headRef, comparisonMode string) (agent.AgentReviewSnapshot, error) {
	if fake.snapshot == nil {
		return agent.AgentReviewSnapshot{}, runtimecontrol.ErrDaemonReviewSnapshotUnavailable
	}
	return *fake.snapshot, fake.snapshotErr
}

func (fake *fakeRuntime) GetAgentReviewFingerprint(ctx context.Context, machineID, agentID, targetPath, baseRef, headRef, comparisonMode string) (agent.AgentReviewFingerprint, error) {
	// Explicit error injection takes priority over the capability-miss default.
	// This lets tests drive the typed-sentinel → HTTP mapping (e.g.
	// ErrReviewTargetUnsupported → 422) without conflating the unsupported
	// outcome with the desktop-too-old 501 case.
	if fake.fingerprintErr != nil {
		return agent.AgentReviewFingerprint{}, fake.fingerprintErr
	}
	if fake.fingerprint == nil {
		return agent.AgentReviewFingerprint{}, runtimecontrol.ErrDaemonReviewFingerprintUnavailable
	}
	return *fake.fingerprint, fake.fingerprintErr
}

func (fake *fakeRuntime) ListAgentReviewFiles(ctx context.Context, machineID, agentID, targetPath, scope, baseRef, headRef, comparisonMode string) ([]agent.AgentReviewFile, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastRemoteRequest = runtimecontrol.RemoteRequestContextFromContext(ctx)
	fake.lastFiles = reviewCall{MachineID: machineID, AgentID: agentID, TargetPath: targetPath, Scope: scope, BaseRef: baseRef, HeadRef: headRef, ComparisonMode: comparisonMode}
	return fake.reviewFiles, fake.reviewFilesErr
}

func (fake *fakeRuntime) GetAgentReviewDiff(ctx context.Context, machineID, agentID, targetPath, scope, path, baseRef, headRef, comparisonMode string) (agent.AgentReviewDiff, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastRemoteRequest = runtimecontrol.RemoteRequestContextFromContext(ctx)
	fake.lastDiff = diffCall{reviewCall: reviewCall{MachineID: machineID, AgentID: agentID, TargetPath: targetPath, Scope: scope, BaseRef: baseRef, HeadRef: headRef, ComparisonMode: comparisonMode}, Path: path}
	return fake.diff, fake.diffErr
}

func (fake *fakeRuntime) GetAgentReviewDiffBatch(ctx context.Context, machineID, agentID, targetPath, scope string, paths []string, baseRef, headRef, comparisonMode string) (map[string]daemon.AgentReviewDiffBatchResult, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastRemoteRequest = runtimecontrol.RemoteRequestContextFromContext(ctx)
	fake.lastDiff = diffCall{reviewCall: reviewCall{MachineID: machineID, AgentID: agentID, TargetPath: targetPath, Scope: scope, BaseRef: baseRef, HeadRef: headRef, ComparisonMode: comparisonMode}, Path: ""}
	// diffBatchErr models a top-level batch error (capability miss,
	// machine offline, repo unsupported) and is distinct from diffErr,
	// which models a per-path failure. Splitting them lets the handler
	// tests prove the top-level error path surfaces as HTTP 422/503/...
	// instead of being silently swallowed into the per-path Errors map.
	if fake.diffBatchErr != nil {
		return nil, fake.diffBatchErr
	}
	results := make(map[string]daemon.AgentReviewDiffBatchResult, len(paths))
	for _, path := range paths {
		if fake.diffErr != nil {
			results[path] = daemon.AgentReviewDiffBatchResult{Error: fake.diffErr}
			continue
		}
		results[path] = daemon.AgentReviewDiffBatchResult{Diff: fake.diff}
	}
	return results, nil
}

func (fake *fakeRuntime) ValidateAgentOutputTarget(ctx context.Context, machineID, agentID, kind, targetPath string) error {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastRemoteRequest = runtimecontrol.RemoteRequestContextFromContext(ctx)
	fake.lastValidate = validateCall{machineID, agentID, kind, targetPath}
	return fake.validateErr
}

func (fake *fakeRuntime) GetAgentTrajectoryHistory(ctx context.Context, machineID string, agentID string, limit int, before int64) ([]runtimecontrol.AgentTrajectoryItem, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastRemoteRequest = runtimecontrol.RemoteRequestContextFromContext(ctx)
	fake.lastTrajectory = trajectoryCall{MachineID: machineID, AgentID: agentID, Limit: limit, Before: before}
	return fake.trajectoryItems, fake.trajectoryErr
}

func (fake *fakeRuntime) GCRecoveryPoints(machineID, agentID string, points []rewind.MessageRecoveryPoint) error {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.gcCalls = append(fake.gcCalls, gcCall{MachineID: machineID, AgentID: agentID, Points: points})
	return fake.gcErr
}

type fakeRealtime struct {
	createdWorkspace string
	deletedWorkspace string
	activityCalls    []activityCall
}

type activityCall struct{ WorkspaceID, OwnerUserID, AgentID, Status, Detail string }

func (fake *fakeRealtime) EmitAgentCreated(workspaceID string) { fake.createdWorkspace = workspaceID }
func (fake *fakeRealtime) EmitAgentDeleted(workspaceID string) { fake.deletedWorkspace = workspaceID }
func (fake *fakeRealtime) EmitAgentActivity(workspaceID, ownerUserID, agentID, status, detail string) {
	fake.activityCalls = append(fake.activityCalls, activityCall{workspaceID, ownerUserID, agentID, status, detail})
}

type fakeOrganizationProvider struct {
	providers []organization.OrganizationProviderView
}

func (fake fakeOrganizationProvider) ListVisibleOrganizationProvidersForWorkspace(context.Context, string) ([]organization.OrganizationProviderView, error) {
	return fake.providers, nil
}

func (fake fakeOrganizationProvider) CreateInternalWorkspaceProxyKey(_ context.Context, workspaceID string, _ time.Time) (string, error) {
	return "workspace-proxy-token-" + workspaceID, nil
}

var _ AgentStore = (*fakeStore)(nil)
var _ ModelProfileStore = (*fakeStore)(nil)
var _ ReviewStore = (*fakeStore)(nil)
var _ RewindStore = (*fakeStore)(nil)
var _ api.SessionAuthenticator = (*fakeStore)(nil)
var _ api.WorkspaceResolver = (*fakeStore)(nil)
var _ Runtime = (*fakeRuntime)(nil)
var _ Realtime = (*fakeRealtime)(nil)
var _ OrganizationProvider = fakeOrganizationProvider{}
