package app

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/access"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

type ServiceEventActor struct {
	ActorType        string
	ActorID          string
	MemberID         string
	AuthorizerUserID string
}

const (
	// owner_member_id_source is a server-owned marker. Only subscriptions that
	// were created after resolving a live agent -> backing machine -> active
	// human owner may carry this value. Deleted-agent fallback must fail closed
	// for every unmarked row because legacy rows may have snapshotted the app
	// authorizer rather than the machine owner.
	serviceEventSubscriptionOwnerMemberIDSourceMetadataKey    = "owner_member_id_source"
	serviceEventSubscriptionOwnerMemberIDSourceAgentMachineV1 = "agent_machine_owner_v1"
)

func serviceEventActorPrincipalMemberID(actor ServiceEventActor) string {
	return firstNonEmpty(actor.AuthorizerUserID, actor.MemberID)
}

func requireServiceEventActorAuthorization(ctx context.Context, queryer coredb.QueryExecer, workspaceID string, actor ServiceEventActor, installationID string, requiredScopes []string) error {
	if strings.TrimSpace(actor.ActorType) == "human" {
		return requireHumanInstallationUse(ctx, queryer, workspaceID, actor.MemberID, installationID)
	}
	if strings.TrimSpace(actor.ActorType) != "agent" {
		return nil
	}
	_, err := checkAgentAppAuthorization(ctx, queryer, workspaceID, actor.ActorID, installationID, requiredScopes)
	return err
}

// requireHumanInstallationUse fences human service-event operations on
// personal installations behind the current ACL: only the owner or holders
// of an active human/workspace grant may act. Workspace installations keep
// their legacy human authority paths.
func requireHumanInstallationUse(ctx context.Context, queryer coredb.QueryExecer, workspaceID string, memberID string, installationID string) error {
	memberID = strings.TrimSpace(memberID)
	if memberID == "" {
		return badRequest("service event human member is required")
	}
	txStore := serviceactor.TxStoreFrom(queryer)
	err := txStore.RequireHumanInstallationUseInTx(ctx, workspaceID, memberID, installationID)
	if err != nil {
		if errors.Is(err, serviceactor.ErrNotFound) {
			return storeerr.NotFound("app_installation_not_authorized")
		}
		return err
	}
	return nil
}

type serviceEventPrincipal struct {
	UserID             string
	MemberID           string
	CanManageWorkspace bool
}

func serviceEventActorPrincipal(ctx context.Context, queryer coredb.QueryRower, workspaceID string, actor ServiceEventActor) (serviceEventPrincipal, error) {
	switch strings.TrimSpace(actor.ActorType) {
	case "human":
		userID := strings.TrimSpace(actor.ActorID)
		memberID := strings.TrimSpace(actor.MemberID)
		if userID == "" || memberID == "" {
			return serviceEventPrincipal{}, badRequest("service event human principal is required")
		}
		row := queryer.QueryRow(ctx, `
SELECT workspace_role
FROM members
WHERE workspace_id = $1
  AND id = $2
  AND user_id = $3
  AND type = 'human'
  AND status = 'active'
LIMIT 1
`, strings.TrimSpace(workspaceID), memberID, userID)
		var workspaceRole string
		if err := row.Scan(&workspaceRole); err != nil {
			if coredb.IsNotFound(err) {
				return serviceEventPrincipal{}, storeerr.NotFound("service_event_principal_not_found")
			}
			return serviceEventPrincipal{}, fmt.Errorf("load service event human principal: %w", err)
		}
		return serviceEventPrincipal{
			UserID:             userID,
			MemberID:           memberID,
			CanManageWorkspace: access.HasWorkspaceAdminRole(domain.WorkspaceRole(strings.TrimSpace(workspaceRole))),
		}, nil
	case "agent":
		memberID, err := serviceEventPrincipalMemberID(ctx, queryer, workspaceID, actor)
		if err != nil {
			return serviceEventPrincipal{}, err
		}
		userID, found, err := serviceEventHumanUserIDForMember(ctx, queryer, memberID)
		if err != nil {
			return serviceEventPrincipal{}, err
		}
		if !found {
			return serviceEventPrincipal{}, badRequest("service event human principal is required")
		}
		return serviceEventPrincipal{UserID: userID, MemberID: memberID}, nil
	case "system", "migration":
		return serviceEventPrincipal{UserID: strings.TrimSpace(actor.ActorID), MemberID: strings.TrimSpace(actor.MemberID)}, nil
	default:
		return serviceEventPrincipal{}, badRequest("actor type is not supported")
	}
}

func requireServiceEventActorOwnsSubscription(ctx context.Context, queryer coredb.QueryRower, workspaceID string, actor ServiceEventActor, subscription ServiceEventSubscriptionRecord) error {
	principal, err := serviceEventActorPrincipal(ctx, queryer, workspaceID, actor)
	if err != nil {
		return err
	}
	// Workspace owner/admin authority is a human-only recovery and management
	// path. An agent owned by an administrator remains scoped to its human
	// principal; it does not inherit workspace-wide subscription access.
	if strings.TrimSpace(actor.ActorType) == "human" && principal.CanManageWorkspace {
		return nil
	}
	// owner_member_id is canonical for new rows, but older or externally
	// authorized agent rows may have captured the app authorizer instead of the
	// machine owner. The immutable creator actor lets us recover the actual agent
	// owner without exposing subscriptions created by somebody else's live
	// agent. Once the creator row is physically deleted, the helper below falls
	// back only to the canonical owner snapshot captured at creation.
	if strings.TrimSpace(actor.ActorType) == "human" && strings.TrimSpace(subscription.CreatedByActorType) == "agent" {
		owned, err := serviceEventSubscriptionCreatedByAgentOwnedByHuman(ctx, queryer, workspaceID, principal.UserID, principal.MemberID, subscription)
		if err != nil {
			return err
		}
		if owned {
			return nil
		}
		return storeerr.NotFound("subscription_not_found")
	}
	if strings.TrimSpace(subscription.OwnerUserID) == principal.UserID ||
		strings.TrimSpace(subscription.CreatedByMemberID) == principal.MemberID {
		return nil
	}
	return storeerr.NotFound("subscription_not_found")
}

func serviceEventSubscriptionCreatedByAgentOwnedByHuman(ctx context.Context, queryer coredb.QueryRower, workspaceID string, userID string, memberID string, subscription ServiceEventSubscriptionRecord) (bool, error) {
	if strings.TrimSpace(subscription.CreatedByActorType) != "agent" || strings.TrimSpace(userID) == "" {
		return false, nil
	}
	ownerUserID, found, err := serviceEventAgentOwnerUserID(ctx, queryer, workspaceID, subscription.CreatedByActorID)
	if err != nil {
		return false, err
	}
	if found {
		return ownerUserID == strings.TrimSpace(userID), nil
	}
	creatorExists, err := serviceEventAgentExists(ctx, queryer, workspaceID, subscription.CreatedByActorID)
	if err != nil {
		return false, err
	}
	if creatorExists {
		// A live creator agent must resolve through its current backing machine.
		// Never let a copied owner snapshot override a different live owner.
		return false, nil
	}
	if !serviceEventSubscriptionHasAuthoritativeAgentMachineOwner(subscription) {
		// Unmarked rows include legacy data where owner_member_id could contain
		// the app authorizer. Trusting that snapshot after agent deletion would
		// transfer visibility and mutation authority, so fail closed.
		return false, nil
	}
	// Canonical writes snapshot the backing machine owner into owner_member_id.
	// Once the creator agent has been physically deleted there is no live owner
	// relation left to resolve, so this immutable snapshot is the only stable
	// lifecycle owner. This is not a migration or repair guarantee for legacy
	// rows whose owner snapshot may have been written incorrectly.
	return strings.TrimSpace(subscription.OwnerUserID) == strings.TrimSpace(userID) ||
		strings.TrimSpace(subscription.CreatedByMemberID) == strings.TrimSpace(memberID), nil
}

func serviceEventSubscriptionHasAuthoritativeAgentMachineOwner(subscription ServiceEventSubscriptionRecord) bool {
	if strings.TrimSpace(subscription.CreatedByActorType) != "agent" {
		return false
	}
	metadata := decodeServiceEventSubscriptionJSONMap(subscription.Metadata)
	return strings.TrimSpace(serviceEventSubscriptionMetadataString(metadata, serviceEventSubscriptionOwnerMemberIDSourceMetadataKey)) == serviceEventSubscriptionOwnerMemberIDSourceAgentMachineV1
}

func serviceEventPrincipalMemberID(ctx context.Context, queryer coredb.QueryRower, workspaceID string, actor ServiceEventActor) (string, error) {
	switch strings.TrimSpace(actor.ActorType) {
	case "human":
		principal, err := serviceEventActorPrincipal(ctx, queryer, workspaceID, actor)
		if err != nil {
			return "", err
		}
		return principal.MemberID, nil
	case "agent":
		// Agent ownership belongs to the user who owns the backing machine. The
		// app authorization's authorizer and the agent member are audit/routing
		// facts, never ownership fallbacks.
		if strings.TrimSpace(actor.ActorID) == "" {
			return "", badRequest("service event agent principal is required")
		}
		memberID, found, err := serviceEventAgentOwnerMemberID(ctx, queryer, workspaceID, actor.ActorID)
		if err != nil {
			return "", err
		}
		if found {
			return memberID, nil
		}
		return "", storeerr.NotFound("service_event_principal_not_found")
	}
	for _, memberID := range []string{actor.AuthorizerUserID, actor.MemberID} {
		_, found, err := serviceEventHumanUserIDForMember(ctx, queryer, memberID)
		if err != nil {
			return "", err
		}
		if found {
			return strings.TrimSpace(memberID), nil
		}
	}
	return "", badRequest("service event principal human member is required")
}

func serviceEventPrincipalMemberIDForTarget(ctx context.Context, queryer coredb.QueryRower, workspaceID string, actor ServiceEventActor, target ServiceEventTarget) (string, error) {
	if actorType := strings.TrimSpace(actor.ActorType); actorType == "human" || actorType == "agent" {
		principalMemberID, err := serviceEventPrincipalMemberID(ctx, queryer, workspaceID, actor)
		if err != nil {
			return "", err
		}
		if targetPrincipalMemberID := strings.TrimSpace(target.PrincipalMemberID); targetPrincipalMemberID != "" && targetPrincipalMemberID != principalMemberID {
			return "", storeerr.Forbidden("service_event_target_principal_mismatch")
		}
		return principalMemberID, nil
	}
	if principalMemberID := strings.TrimSpace(target.PrincipalMemberID); principalMemberID != "" {
		_, found, err := serviceEventHumanUserIDForMember(ctx, queryer, principalMemberID)
		if err != nil {
			return "", err
		}
		if !found {
			return "", badRequest("service event target principal human member is required")
		}
		return principalMemberID, nil
	}
	return serviceEventPrincipalMemberID(ctx, queryer, workspaceID, actor)
}

func serviceEventAgentOwnerMemberID(ctx context.Context, queryer coredb.QueryRower, workspaceID string, agentID string) (string, bool, error) {
	trimmedWorkspaceID := strings.TrimSpace(workspaceID)
	trimmedAgentID := strings.TrimSpace(agentID)
	if trimmedWorkspaceID == "" || trimmedAgentID == "" {
		return "", false, nil
	}
	row := queryer.QueryRow(ctx, `
SELECT owner_member.id
FROM agents ag
JOIN machines mac
  ON mac.workspace_id = ag.workspace_id
 AND mac.id = ag.machine_id
JOIN members owner_member
  ON owner_member.workspace_id = ag.workspace_id
 AND owner_member.user_id = mac.created_by_user_id
 AND owner_member.type = 'human'
 AND owner_member.status = 'active'
WHERE ag.workspace_id = $1
  AND ag.id = $2
LIMIT 1
`, trimmedWorkspaceID, trimmedAgentID)
	var memberID string
	if err := row.Scan(&memberID); err != nil {
		if coredb.IsNotFound(err) {
			return "", false, nil
		}
		return "", false, fmt.Errorf("load service event owner agent member: %w", err)
	}
	return strings.TrimSpace(memberID), strings.TrimSpace(memberID) != "", nil
}

func serviceEventAgentOwnerUserID(ctx context.Context, queryer coredb.QueryRower, workspaceID string, agentID string) (string, bool, error) {
	trimmedWorkspaceID := strings.TrimSpace(workspaceID)
	trimmedAgentID := strings.TrimSpace(agentID)
	if trimmedWorkspaceID == "" || trimmedAgentID == "" {
		return "", false, nil
	}
	row := queryer.QueryRow(ctx, `
SELECT mac.created_by_user_id
FROM agents ag
JOIN machines mac
  ON mac.workspace_id = ag.workspace_id
 AND mac.id = ag.machine_id
WHERE ag.workspace_id = $1
  AND ag.id = $2
LIMIT 1
`, trimmedWorkspaceID, trimmedAgentID)
	var userID string
	if err := row.Scan(&userID); err != nil {
		if coredb.IsNotFound(err) {
			return "", false, nil
		}
		return "", false, fmt.Errorf("load service event owner agent user: %w", err)
	}
	return strings.TrimSpace(userID), strings.TrimSpace(userID) != "", nil
}

func serviceEventAgentExists(ctx context.Context, queryer coredb.QueryRower, workspaceID string, agentID string) (bool, error) {
	trimmedWorkspaceID := strings.TrimSpace(workspaceID)
	trimmedAgentID := strings.TrimSpace(agentID)
	if trimmedWorkspaceID == "" || trimmedAgentID == "" {
		return false, nil
	}
	row := queryer.QueryRow(ctx, `
SELECT 1
FROM agents
WHERE workspace_id = $1
  AND id = $2
LIMIT 1
`, trimmedWorkspaceID, trimmedAgentID)
	var marker int
	if err := row.Scan(&marker); err != nil {
		if coredb.IsNotFound(err) {
			return false, nil
		}
		return false, fmt.Errorf("check service event creator agent existence: %w", err)
	}
	return true, nil
}

func serviceEventHumanUserIDForMember(ctx context.Context, queryer coredb.QueryRower, memberID string) (string, bool, error) {
	trimmedMemberID := strings.TrimSpace(memberID)
	if trimmedMemberID == "" {
		return "", false, nil
	}
	row := queryer.QueryRow(ctx, `
SELECT user_id
FROM members
WHERE id = $1
  AND type = 'human'
  AND user_id <> ''
LIMIT 1
`, trimmedMemberID)
	var userID string
	if err := row.Scan(&userID); err != nil {
		if coredb.IsNotFound(err) {
			return "", false, nil
		}
		return "", false, fmt.Errorf("load service event owner user: %w", err)
	}
	return strings.TrimSpace(userID), strings.TrimSpace(userID) != "", nil
}

func validateServiceEventActor(actor ServiceEventActor) error {
	if strings.TrimSpace(actor.ActorType) == "" || strings.TrimSpace(actor.ActorID) == "" || strings.TrimSpace(actor.MemberID) == "" {
		return badRequest("actor type, actor id, and member id are required")
	}
	switch strings.TrimSpace(actor.ActorType) {
	case "human", "agent", "system", "migration":
		return nil
	default:
		return badRequest("actor type is not supported")
	}
}
