package server

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/config"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/forwarder"
	"github.com/tidbcloud/AgentTeam/internal/service/role/execution"
	"github.com/tidbcloud/AgentTeam/internal/store/approval"
	executionstore "github.com/tidbcloud/AgentTeam/internal/store/role/execution"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
)

type roleManagementOperationGateway interface {
	Preflight(
		context.Context,
		forwarder.RoleOperationPrincipal,
		forwarder.RoleOperationRequest,
	) error
	Execute(
		context.Context,
		forwarder.RoleOperationPrincipal,
		forwarder.RoleOperationExecuteRequest,
	) (forwarder.RoleOperationResult, error)
	Recover(
		context.Context,
		forwarder.RoleOperationPrincipal,
		forwarder.RoleOperationExecuteRequest,
	) (forwarder.RoleOperationRecoveryResult, error)
}

type roleManagementWorkspaceLister interface {
	ListRoutableWorkspaceIDsForGroup(context.Context, string) ([]string, error)
}

type roleManagementWorkspaceIDSource func(context.Context) ([]string, error)

type roleManagementRuntime struct {
	service *execution.Service
	worker  *execution.Worker
	manager execution.CoreSubmissionStore
}

type roleManagementRuntimeOptions struct {
	Config                config.Config
	DB                    coredb.DB
	Approvals             *approval.Store
	WorkspaceLister       roleManagementWorkspaceLister
	Gateway               roleManagementOperationGateway
	LoadProjectedTerminal func(context.Context, approval.Instance, execution.ApprovalPayload) (execution.RequestResult, bool, error)
	ProjectTerminal       func(context.Context, approval.Instance, execution.RequestResult) error
	Logger                *slog.Logger
}

func preflightRoleManagementOperation(
	ctx context.Context,
	gateway roleManagementOperationGateway,
	command execution.PreflightCommand,
) error {
	request, err := roleManagementOperationRequest(command.Payload)
	if err != nil {
		return err
	}
	ownedAgentIDs := []string(nil)
	if command.Payload.Apply != nil {
		ownedAgentIDs = []string{command.Payload.Apply.AgentID}
	}
	if err := gateway.Preflight(ctx, forwarder.RoleOperationPrincipal{
		WorkspaceID:   command.Principal.WorkspaceID,
		Subject:       command.Manager.UserID,
		OwnedAgentIDs: ownedAgentIDs,
	}, request); err != nil {
		return mapRoleManagementOperationError(err)
	}
	return nil
}

func applyRoleManagementOperation(
	ctx context.Context,
	gateway roleManagementOperationGateway,
	command execution.ApplyCommand,
) (executionstore.Success, error) {
	request, err := roleManagementOperationRequest(command.Payload)
	if err != nil {
		return executionstore.Success{}, err
	}
	result, err := gateway.Execute(ctx, forwarder.RoleOperationPrincipal{
		WorkspaceID:   command.Approval.WorkspaceID,
		Subject:       command.Authority.ManagerUserID,
		OwnedAgentIDs: command.Authority.OwnedAgentIDs,
	}, forwarder.RoleOperationExecuteRequest{
		OperationKey:         command.OperationKey,
		RequestDigest:        command.RequestDigest,
		RoleOperationRequest: request,
	})
	if err != nil {
		return executionstore.Success{}, mapRoleManagementOperationError(err)
	}
	return executionstore.Success{
		RoleID: result.RoleID, RoleVersionID: result.RoleVersionID,
	}, nil
}

func recoverRoleManagementOperation(
	ctx context.Context,
	gateway roleManagementOperationGateway,
	command execution.RecoverAppliedCommand,
) (executionstore.Success, bool, error) {
	request, err := roleManagementOperationRequest(command.Payload)
	if err != nil {
		return executionstore.Success{}, false, err
	}
	result, err := gateway.Recover(ctx, forwarder.RoleOperationPrincipal{
		WorkspaceID: command.Approval.WorkspaceID,
		// Receipt recovery is deliberately independent of current manager
		// authority. The immutable deciding member is only the typed internal
		// transport subject; MeshX recovery authorizes no mutation.
		Subject: command.Approval.DecidedByMemberID,
	}, forwarder.RoleOperationExecuteRequest{
		OperationKey:         command.OperationKey,
		RequestDigest:        command.RequestDigest,
		RoleOperationRequest: request,
	})
	if err != nil {
		return executionstore.Success{}, false, mapRoleManagementOperationError(err)
	}
	return executionstore.Success{
		RoleID: result.RoleID, RoleVersionID: result.RoleVersionID,
	}, result.Found, nil
}

func roleManagementWorkspaceIDsForConfig(
	cfg config.Config,
	db coredb.Queryer,
	lister roleManagementWorkspaceLister,
) (roleManagementWorkspaceIDSource, error) {
	profile := cfg.Server.ProfileOrDefault()
	if !cfg.MeshX.Enabled || profile == config.ServerProfileCentral || profile == config.ServerProfileExtension {
		// Central and the worker-free Extension profile do not run Role
		// management workspace reconciliation (spec §6.1).
		return nil, nil
	}
	if err := cfg.Server.Validate(); err != nil {
		return nil, err
	}
	switch profile {
	case config.ServerProfileAll:
		return func(ctx context.Context) ([]string, error) {
			return workspace.ListWorkspaceIDs(ctx, db)
		}, nil
	case config.ServerProfileWorkspace:
		groupID := strings.TrimSpace(cfg.Server.WorkspaceGroupID)
		return func(ctx context.Context) ([]string, error) {
			if lister == nil {
				return nil, errors.New(
					"Role management workspace lister is not configured",
				)
			}
			return lister.ListRoutableWorkspaceIDsForGroup(ctx, groupID)
		}, nil
	default:
		return nil, fmt.Errorf(
			"unsupported Role management server profile %q",
			cfg.Server.ProfileOrDefault(),
		)
	}
}

func newRoleManagementRuntimeForConfig(
	options roleManagementRuntimeOptions,
) (*roleManagementRuntime, error) {
	workspaceIDs, err := roleManagementWorkspaceIDsForConfig(
		options.Config, options.DB, options.WorkspaceLister,
	)
	if err != nil {
		return nil, err
	}
	if workspaceIDs == nil {
		return nil, nil
	}
	if options.DB == nil || options.Approvals == nil ||
		options.Gateway == nil || options.LoadProjectedTerminal == nil ||
		options.ProjectTerminal == nil ||
		(options.WorkspaceLister == nil &&
			options.Config.Server.ProfileOrDefault() == config.ServerProfileWorkspace) {
		return nil, errors.New(
			"Role management runtime dependencies are incomplete",
		)
	}

	coreStore := execution.CoreSubmissionStore{
		DB: options.DB, Approvals: options.Approvals,
	}
	executionStore := executionstore.New(options.DB)
	service := execution.NewService(execution.Dependencies{
		AuthenticateApplicant: coreStore.AuthenticateApplicant,
		LookupApproval:        coreStore.LookupApproval,
		ListAssignments:       coreStore.ListAssignments,
		ResolveManager:        coreStore.ResolveManager,
		Preflight: func(
			ctx context.Context,
			command execution.PreflightCommand,
		) error {
			return preflightRoleManagementOperation(
				ctx, options.Gateway, command,
			)
		},
		CommitSubmission: coreStore.CommitSubmission,
		DecideWithEffect: options.Approvals.DecideWithEffect,
		EnqueueInTx: func(
			ctx context.Context,
			tx coredb.Tx,
			workspaceID, approvalID string,
			now time.Time,
		) (executionstore.Execution, bool, error) {
			return executionStore.InTx(tx).Enqueue(
				ctx, workspaceID, approvalID, now,
			)
		},
		LoadExecution:         executionStore.LoadByApprovalID,
		LoadApproval:          options.Approvals.Load,
		LoadProjectedTerminal: options.LoadProjectedTerminal,
		RecoverApplied: func(
			ctx context.Context,
			command execution.RecoverAppliedCommand,
		) (executionstore.Success, bool, error) {
			return recoverRoleManagementOperation(
				ctx, options.Gateway, command,
			)
		},
		RecheckExecutionAuthority: coreStore.RecheckExecutionAuthority,
		ProjectTerminal:           options.ProjectTerminal,
		Apply: func(
			ctx context.Context,
			command execution.ApplyCommand,
		) (executionstore.Success, error) {
			return applyRoleManagementOperation(
				ctx, options.Gateway, command,
			)
		},
		Succeed: func(
			ctx context.Context,
			fence executionstore.ExecutionFence,
			success executionstore.Success,
			now time.Time,
		) error {
			_, err := executionStore.Succeed(ctx, fence, success, now)
			return err
		},
		ReleaseUncertain: func(
			ctx context.Context,
			fence executionstore.ExecutionFence,
			now time.Time,
		) error {
			_, err := executionStore.ReleaseUncertain(ctx, fence, now)
			return err
		},
		FailDeterministic: func(
			ctx context.Context,
			fence executionstore.ExecutionFence,
			failure executionstore.Failure,
			now time.Time,
		) error {
			_, err := executionStore.FailDeterministic(
				ctx, fence, failure, now,
			)
			return err
		},
		Now: time.Now,
	})
	worker, err := execution.NewWorker(execution.WorkerOptions{
		WorkspaceIDs:  workspaceIDs,
		ClaimRunnable: executionStore.ClaimRunnable,
		Execute:       service.Execute,
		Logger:        options.Logger,
	})
	if err != nil {
		return nil, fmt.Errorf("build Role management worker: %w", err)
	}
	return &roleManagementRuntime{
		service: service, worker: worker, manager: coreStore,
	}, nil
}

func roleManagementOperationRequest(
	payload execution.ApprovalPayload,
) (forwarder.RoleOperationRequest, error) {
	var request forwarder.RoleOperationRequest
	branchCount := 0
	if payload.Create != nil {
		branchCount++
		request.Create = &forwarder.RoleOperationCreateRequest{
			Name: payload.Create.Name, Definition: payload.Create.Definition,
			Description: payload.Create.Description,
		}
	}
	if payload.Update != nil {
		branchCount++
		var visibility *string
		if payload.Update.Visibility != "" {
			value := payload.Update.Visibility
			visibility = &value
		}
		request.Update = &forwarder.RoleOperationUpdateRequest{
			RoleID:                payload.Update.RoleID,
			ExpectedRoleVersionID: payload.Update.ExpectedRoleVersionID,
			Name:                  payload.Update.Name,
			Description:           payload.Update.Description,
			Definition:            payload.Update.Definition,
			Visibility:            visibility,
		}
	}
	if payload.Delete != nil {
		branchCount++
		request.Delete = &forwarder.RoleOperationDeleteRequest{
			RoleID:                payload.Delete.RoleID,
			ExpectedRoleVersionID: payload.Delete.ExpectedRoleVersionID,
		}
	}
	if payload.Apply != nil {
		branchCount++
		request.Apply = &forwarder.RoleOperationApplyRequest{
			RoleID: payload.Apply.RoleID, RoleVersionID: payload.Apply.RoleVersionID,
			AgentID: payload.Apply.AgentID,
		}
	}
	if branchCount != 1 ||
		(request.Create != nil && payload.Action != execution.ActionCreate) ||
		(request.Update != nil && payload.Action != execution.ActionUpdate) ||
		(request.Delete != nil && payload.Action != execution.ActionDelete) ||
		(request.Apply != nil && payload.Action != execution.ActionApply) {
		return forwarder.RoleOperationRequest{}, execution.ErrInvariant
	}
	return request, nil
}

func mapRoleManagementOperationError(err error) error {
	var deterministic *forwarder.DeterministicRoleOperationError
	if !errors.As(err, &deterministic) || deterministic == nil {
		return err
	}
	var outcome execution.ApplyOutcome
	switch deterministic.Code {
	case "invalid_request":
		outcome = execution.ApplyInvalidRequest
	case "authority_denied":
		outcome = execution.ApplyAuthorityDenied
	case "not_found":
		outcome = execution.ApplyNotFound
	case "role_version_conflict":
		outcome = execution.ApplyVersionConflict
	case "role_operation_conflict":
		outcome = execution.ApplyOperationConflict
	default:
		return err
	}
	return &execution.ApplyError{Outcome: outcome}
}
