package access

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

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
)

var ErrInvalidAuthorizationRequest = errors.New("invalid authorization request")

type ResourceKind string

const (
	ResourceWorkspace           ResourceKind = "workspace"
	ResourceChannel             ResourceKind = "channel"
	ResourceDM                  ResourceKind = "dm"
	ResourceThread              ResourceKind = "thread"
	ResourceMachine             ResourceKind = "machine"
	ResourceAgent               ResourceKind = "agent"
	ResourceServiceInstallation ResourceKind = "service_installation"
)

type Action string

const (
	ActionAccess            Action = "access"
	ActionRead              Action = "read"
	ActionPost              Action = "post"
	ActionInviteMembers     Action = "invite_members"
	ActionManageMembers     Action = "manage_members"
	ActionManageMemberRoles Action = "manage_member_roles"
	ActionManageRuntime     Action = "manage_runtime"
	ActionConnect           Action = "connect"
	ActionInstallService    Action = "install_service"
	ActionManage            Action = "manage"
	ActionOperate           Action = "operate"
	ActionReadWorkspace     Action = "read_workspace"
	ActionActAsMember       Action = "act_as_member"
	ActionApprove           Action = "approve"
	ActionInvoke            Action = "invoke"
)

// HumanAction is one stable authorization question asked against the core-owned
// workspace role facts for a human member.
type HumanAction struct {
	Resource ResourceKind
	Action   Action
}

func LoadAuthorizedHumanWorkspaceMember(ctx context.Context, queryer coredb.QueryRower, userID string, workspaceID string, action HumanAction) (HumanWorkspaceMember, error) {
	membership, err := LoadHumanWorkspaceMember(ctx, queryer, userID, workspaceID)
	if err != nil {
		return HumanWorkspaceMember{}, err
	}
	request := NewHumanAuthorizationRequest(userID, workspaceID, membership.Member.ID, workspaceID, action)
	if _, err := DecideHumanWorkspaceRoleAuthorization(membership, request); err != nil {
		return HumanWorkspaceMember{}, err
	}
	return membership, nil
}

func LoadAuthorizedHumanWorkspaceMemberForUpdate(ctx context.Context, queryer coredb.QueryRower, userID string, workspaceID string, action HumanAction) (HumanWorkspaceMember, error) {
	membership, err := LoadHumanWorkspaceMemberForUpdate(ctx, queryer, userID, workspaceID)
	if err != nil {
		return HumanWorkspaceMember{}, err
	}
	request := NewHumanAuthorizationRequest(userID, workspaceID, membership.Member.ID, workspaceID, action)
	if _, err := DecideHumanWorkspaceRoleAuthorization(membership, request); err != nil {
		return HumanWorkspaceMember{}, err
	}
	return membership, nil
}

func LoadAuthorizedHumanChannelAccess(ctx context.Context, queryer coredb.QueryRower, userID string, workspaceID string, channelID string, action HumanAction) (HumanChannelAccess, error) {
	channelAccess, err := LoadHumanChannelAccess(ctx, queryer, userID, workspaceID, channelID)
	if err != nil {
		return HumanChannelAccess{}, err
	}
	request := NewHumanAuthorizationRequest(userID, workspaceID, channelAccess.WorkspaceMember.Member.ID, channelID, action)
	if _, err := DecideHumanChannelAuthorization(channelAccess, request); err != nil {
		return HumanChannelAccess{}, err
	}
	return channelAccess, nil
}

func LoadAuthorizedHumanDMAccess(ctx context.Context, queryer coredb.QueryRower, userID string, workspaceID string, dmThreadID string, action HumanAction) (HumanDMAccess, error) {
	dmAccess, err := LoadHumanDMAccess(ctx, queryer, userID, workspaceID, dmThreadID)
	if err != nil {
		return HumanDMAccess{}, err
	}
	request := NewHumanAuthorizationRequest(userID, workspaceID, dmAccess.WorkspaceMember.Member.ID, dmThreadID, action)
	if _, err := DecideHumanDMAuthorization(dmAccess, request); err != nil {
		return HumanDMAccess{}, err
	}
	return dmAccess, nil
}

func LoadAuthorizedHumanThreadAccess(ctx context.Context, queryer coredb.QueryRower, userID string, workspaceID string, threadChannelID string, action HumanAction) (HumanThreadAccess, error) {
	threadAccess, err := LoadHumanThreadAccess(ctx, queryer, userID, workspaceID, threadChannelID)
	if err != nil {
		return HumanThreadAccess{}, err
	}
	request := NewHumanAuthorizationRequest(userID, workspaceID, threadAccess.WorkspaceMember.Member.ID, threadChannelID, action)
	if _, err := DecideHumanThreadAuthorization(threadAccess, request); err != nil {
		return HumanThreadAccess{}, err
	}
	return threadAccess, nil
}

func LoadAuthorizedHumanMachineAccess(ctx context.Context, queryer coredb.QueryRower, userID string, workspaceID string, machineID string, action HumanAction) (HumanMachineAccess, error) {
	machineAccess, err := LoadHumanMachineAccess(ctx, queryer, userID, workspaceID, machineID)
	if err != nil {
		return HumanMachineAccess{}, err
	}
	request := NewHumanAuthorizationRequest(userID, workspaceID, machineAccess.WorkspaceMember.Member.ID, machineID, action)
	if _, err := DecideHumanMachineAuthorization(machineAccess, request); err != nil {
		return HumanMachineAccess{}, err
	}
	return machineAccess, nil
}

func LoadAuthorizedHumanAgentAccess(ctx context.Context, queryer coredb.QueryRower, userID string, workspaceID string, agentID string, action HumanAction) (HumanAgentAccess, error) {
	agentAccess, err := LoadHumanAgentAccess(ctx, queryer, userID, workspaceID, agentID)
	if err != nil {
		return HumanAgentAccess{}, err
	}
	request := NewHumanAuthorizationRequest(userID, workspaceID, agentAccess.WorkspaceMember.Member.ID, agentID, action)
	if _, err := DecideHumanAgentAuthorization(agentAccess, request); err != nil {
		return HumanAgentAccess{}, err
	}
	return agentAccess, nil
}

func AuthorizeHumanAction(membership HumanWorkspaceMember, action HumanAction) error {
	return AuthorizeWorkspaceRole(domain.WorkspaceRole(membership.WorkspaceRole), action)
}

func AuthorizeHumanChannelAction(channelAccess HumanChannelAccess, action HumanAction) error {
	if err := validateHumanChannelAction(action); err != nil {
		return err
	}

	switch action.Action {
	case ActionRead, ActionPost:
		if channelAccess.Joined {
			return nil
		}
	case ActionInviteMembers:
		if channelAccess.CanInvite {
			return nil
		}
	case ActionManageMembers:
		if channelAccess.IsOwner {
			return nil
		}
	}

	return ErrForbidden
}

func AuthorizeHumanDMAction(dmAccess HumanDMAccess, action HumanAction) error {
	if err := validateHumanDMAction(action); err != nil {
		return err
	}
	if dmAccess.Participant {
		return nil
	}
	return ErrForbidden
}

func AuthorizeHumanThreadAction(threadAccess HumanThreadAccess, action HumanAction) error {
	if err := validateHumanThreadAction(action); err != nil {
		return err
	}

	switch threadAccess.ParentThreadType {
	case domain.ThreadTypeChannel:
		if threadAccess.ParentChannelJoined {
			return nil
		}
	case domain.ThreadTypeDM:
		if threadAccess.ParentDMParticipant {
			return nil
		}
	}

	return ErrForbidden
}

func AuthorizeWorkspaceRole(role domain.WorkspaceRole, action HumanAction) error {
	if err := validateHumanAction(action); err != nil {
		return err
	}

	switch action.Resource {
	case ResourceWorkspace:
		switch action.Action {
		case ActionAccess:
			return nil
		case ActionManageMembers, ActionManageRuntime, ActionInstallService:
			if HasWorkspaceAdminRole(role) {
				return nil
			}
			return ErrForbidden
		case ActionManageMemberRoles:
			if IsWorkspaceOwnerRole(role) {
				return nil
			}
			return ErrForbidden
		}
	case ResourceMachine:
		switch action.Action {
		case ActionManage:
			if HasWorkspaceAdminRole(role) {
				return nil
			}
			return ErrForbidden
		}
	case ResourceAgent:
		switch action.Action {
		case ActionOperate, ActionReadWorkspace:
			if HasWorkspaceAdminRole(role) {
				return nil
			}
			return ErrForbidden
		}
	case ResourceServiceInstallation:
		switch action.Action {
		case ActionApprove:
			if HasWorkspaceAdminRole(role) {
				return nil
			}
			return ErrForbidden
		}
	}

	return invalidAuthorizationRequest(action)
}

func validateHumanChannelAction(action HumanAction) error {
	if action.Resource != ResourceChannel {
		return invalidAuthorizationRequest(action)
	}

	switch action.Action {
	case ActionRead, ActionPost, ActionInviteMembers, ActionManageMembers:
		return nil
	default:
		return invalidAuthorizationRequest(action)
	}
}

func validateHumanDMAction(action HumanAction) error {
	if action.Resource != ResourceDM {
		return invalidAuthorizationRequest(action)
	}

	switch action.Action {
	case ActionRead, ActionPost:
		return nil
	default:
		return invalidAuthorizationRequest(action)
	}
}

func validateHumanThreadAction(action HumanAction) error {
	if action.Resource != ResourceThread {
		return invalidAuthorizationRequest(action)
	}

	switch action.Action {
	case ActionRead, ActionPost:
		return nil
	default:
		return invalidAuthorizationRequest(action)
	}
}

func HasWorkspaceAdminRole(role domain.WorkspaceRole) bool {
	switch role {
	case domain.WorkspaceRoleOwner, domain.WorkspaceRoleAdmin:
		return true
	default:
		return false
	}
}

func IsWorkspaceOwnerRole(role domain.WorkspaceRole) bool {
	return role == domain.WorkspaceRoleOwner
}

func validateHumanAction(action HumanAction) error {
	switch action.Resource {
	case ResourceWorkspace:
		switch action.Action {
		case ActionAccess, ActionManageMembers, ActionManageMemberRoles, ActionManageRuntime, ActionInstallService:
			return nil
		}
	case ResourceMachine:
		if action.Action == ActionManage {
			return nil
		}
	case ResourceAgent:
		switch action.Action {
		case ActionOperate, ActionReadWorkspace:
			return nil
		}
	case ResourceServiceInstallation:
		switch action.Action {
		case ActionApprove:
			return nil
		}
	}

	return invalidAuthorizationRequest(action)
}

func invalidAuthorizationRequestFor(resource ResourceKind, action Action) error {
	return fmt.Errorf("%w: unsupported %s.%s", ErrInvalidAuthorizationRequest, resource, action)
}

func invalidAuthorizationRequest(action HumanAction) error {
	return invalidAuthorizationRequestFor(action.Resource, action.Action)
}

// AuthorizeActiveAgentChannelMembership is the single canonical Agent
// channel-membership authorizer shared by every Agent Task entry point. Given a
// channel id resolved server-side from the active Task/board authority, it
// verifies the same-workspace active Agent+Machine+Member currently holds active
// membership on that channel scope. Orphaned/legacy boards, cross-workspace,
// other-channel, deleted/archived/unresolvable, and kind-mismatch cases all fail
// closed and non-enumerating (one uniform forbidden result). A client-supplied
// channel/taskScope value is only a location/consistency hint and never the
// authorization fact.
//
// The security predicates are the SAME single-source fragments used by
// LockActiveAgentChannelAuthority (agent_authority_predicates.go), so a status/
// type/archive/close/visibility fact added to one path but not the other fails
// the structural parity test.
func AuthorizeActiveAgentChannelMembership(ctx context.Context, queryer coredb.QueryRower, workspaceID string, memberID string, channelID string) error {
	workspaceID = strings.TrimSpace(workspaceID)
	memberID = strings.TrimSpace(memberID)
	channelID = strings.TrimSpace(channelID)
	if workspaceID == "" || memberID == "" || channelID == "" {
		return ErrForbidden
	}
	row := queryer.QueryRow(ctx, `
SELECT s.id
FROM scopes s
JOIN scope_memberships sm
  ON `+activeAgentMembershipReadScope+`
 AND `+activeAgentMembershipSecurity+`
JOIN members m
  ON `+activeAgentMemberSecurity+`
JOIN agents a
  ON `+activeAgentSecurity+`
 AND `+activeAgentReadMemberLinkage+`
JOIN machines mac
  ON mac.workspace_id = a.workspace_id
 AND mac.id = a.machine_id
 AND `+activeMachineStatusSecurity+`
WHERE `+activeAgentChannelScopePredicate+`
LIMIT 1
`, workspaceID, channelID, memberID)
	var resolvedID string
	if err := row.Scan(&resolvedID); err != nil {
		if coredb.IsNotFound(err) {
			return ErrForbidden
		}
		return fmt.Errorf("authorize active agent channel membership: %w", err)
	}
	return nil
}

// LockActiveAgentChannelAuthority is the ordered locking fence that makes the
// transaction-side authority identical to AuthorizeActiveAgentChannelMembership.
// It acquires FOR UPDATE locks on the rows whose change removes authority — in
// the canonical lock order Task/board → scope → membership → member → agent →
// machine — revalidating the exact same active predicates on each row. A
// missing/deactivated/deleted/archived/cross-workspace/closed fact fails closed
// non-enumerating (ErrForbidden) and serializes against a concurrent
// membership revoke / archive / effective-close / Channel delete/status change /
// Channel archive / member deactivation / Agent deactivation / Machine
// deactivation.
// LockActiveMemberChannelAuthority is the principal-agnostic ordered membership
// lock: it locks the active non-archived channel scope and the member's active
// non-archived non-effectively-closed scope_memberships row, without requiring
// the member's Agent runtime record to be `status='active'`. It is the lock-time
// authority for an Agent's own task-create on a channel where it is a member:
// a freshly-created (not-yet-activated) Agent whose member row is active and
// whose scope membership is active may create tasks on its own channels — the
// authority boundary is channel membership, not the Agent's runtime activation
// state. A missing/deactivated member, archived/closed membership, or
// missing/archived channel scope still fails closed non-enumerating
// (ErrForbidden). This is deliberately a membership-only fence: operations that
// require an active Agent (waking, receiving, reacting) use
// LockActiveAgentChannelAuthority / AuthorizeActiveAgentChannelMembership, which
// retain the agents.status='active' fact.
func LockActiveMemberChannelAuthority(ctx context.Context, queryer coredb.QueryRower, workspaceID string, memberID string, channelID string) error {
	workspaceID = strings.TrimSpace(workspaceID)
	memberID = strings.TrimSpace(memberID)
	channelID = strings.TrimSpace(channelID)
	if workspaceID == "" || memberID == "" || channelID == "" {
		return ErrForbidden
	}

	// 1. Channel scope row: active, non-archived, same workspace, type channel.
	var scopeID string
	if err := queryer.QueryRow(ctx, `
SELECT s.id
FROM scopes s
WHERE`+activeAgentChannelScopePredicate+`
FOR UPDATE
`, workspaceID, channelID).Scan(&scopeID); err != nil {
		if coredb.IsNotFound(err) {
			return ErrForbidden
		}
		return fmt.Errorf("lock active member channel scope: %w", err)
	}

	// 2. Membership row: active, non-archived, non-effectively-closed.
	var membershipID string
	if err := queryer.QueryRow(ctx, `
SELECT sm.member_id
FROM scope_memberships sm
WHERE `+activeAgentMembershipLockScope+`
  AND `+activeAgentMembershipSecurity+`
FOR UPDATE
`, workspaceID, channelID, memberID).Scan(&membershipID); err != nil {
		if coredb.IsNotFound(err) {
			return ErrForbidden
		}
		return fmt.Errorf("lock active member channel membership: %w", err)
	}

	return nil
}

func LockActiveAgentChannelAuthority(ctx context.Context, queryer coredb.QueryRower, workspaceID string, memberID string, channelID string) error {
	workspaceID = strings.TrimSpace(workspaceID)
	memberID = strings.TrimSpace(memberID)
	channelID = strings.TrimSpace(channelID)
	if workspaceID == "" || memberID == "" || channelID == "" {
		return ErrForbidden
	}

	// 1. Channel scope row: active, non-archived, same workspace, type channel.
	var scopeID string
	if err := queryer.QueryRow(ctx, `
SELECT s.id
FROM scopes s
WHERE`+activeAgentChannelScopePredicate+`
FOR UPDATE
`, workspaceID, channelID).Scan(&scopeID); err != nil {
		if coredb.IsNotFound(err) {
			return ErrForbidden
		}
		return fmt.Errorf("lock active agent channel scope: %w", err)
	}

	// 2. Membership row: active, non-archived, non-effectively-closed.
	var membershipID string
	if err := queryer.QueryRow(ctx, `
SELECT sm.member_id
FROM scope_memberships sm
WHERE `+activeAgentMembershipLockScope+`
  AND `+activeAgentMembershipSecurity+`
FOR UPDATE
`, workspaceID, channelID, memberID).Scan(&membershipID); err != nil {
		if coredb.IsNotFound(err) {
			return ErrForbidden
		}
		return fmt.Errorf("lock active agent channel membership: %w", err)
	}

	// 3. Member row: active agent-typed member. The shared predicate references
	//    the join's param positions ($1 workspaceID, $2 channelID, $3 memberID);
	//    every ordered lock passes the full three-arg set so the identical
	//    predicate compiles without drift.
	var memberRowID string
	if err := queryer.QueryRow(ctx, `
SELECT m.id
FROM members m
WHERE`+activeAgentMemberSecurity+`
FOR UPDATE
`, workspaceID, channelID, memberID).Scan(&memberRowID); err != nil {
		if coredb.IsNotFound(err) {
			return ErrForbidden
		}
		return fmt.Errorf("lock active agent member: %w", err)
	}

	// 4. Agent row: active same-workspace agent; capture machine_id for the next
	//    lock. Uses the shared agent security predicate with the lock member
	//    linkage (member_id = $2).
	var machineID string
	if err := queryer.QueryRow(ctx, `
SELECT COALESCE(a.machine_id, '')
FROM agents a
WHERE `+activeAgentSecurity+`
  AND `+activeAgentLockMemberLinkage+`
FOR UPDATE
`, workspaceID, memberID).Scan(&machineID); err != nil {
		if coredb.IsNotFound(err) {
			return ErrForbidden
		}
		return fmt.Errorf("lock active agent: %w", err)
	}
	machineID = strings.TrimSpace(machineID)
	if machineID == "" {
		return ErrForbidden
	}

	// 5. Machine row: active same-workspace machine. Uses the shared machine
	//    status security predicate with the lock linkage (workspace_id = $1,
	//    id = $2).
	if err := queryer.QueryRow(ctx, `
SELECT mac.id
FROM machines mac
WHERE mac.workspace_id = $1
  AND mac.id = $2
  AND `+activeMachineStatusSecurity+`
FOR UPDATE
`, workspaceID, machineID).Scan(&scopeID); err != nil {
		if coredb.IsNotFound(err) {
			return ErrForbidden
		}
		return fmt.Errorf("lock active agent machine: %w", err)
	}
	return nil
}

// ActiveTaskBoardChannel is the canonical server-resolved Task authority: the
// task's unique Channel derived from the active Task/board authority (the
// task_boards row joined from tasks.board_id, never the denormalized
// tasks.channel_id on its own), plus the canonical Task identity. A
// client-supplied channel/taskScope is only a location/consistency hint and
// never the authorization fact.
type ActiveTaskBoardChannel struct {
	TaskID      string
	TaskScopeID string
	TaskNumber  int64
	ChannelID   string
	Title       string
}

// ResolveActiveTaskBoardChannel resolves the canonical Task->board Channel for
// one task scope. It joins the active Task scope with tasks.board_id ->
// task_boards, derives the board's authoritative active Channel, verifies
// workspace consistency, and fail-closes on null/orphan/legacy boards plus a
// denormalized tasks.channel_id mismatch. Missing or inactive Task scope/board/
// Channel, cross-workspace rows, orphaned boards, and channel_id disagreement
// all fail closed non-enumerating (one uniform ErrForbidden result).
func ResolveActiveTaskBoardChannel(ctx context.Context, queryer coredb.QueryRower, workspaceID string, taskScopeID string) (ActiveTaskBoardChannel, error) {
	workspaceID = strings.TrimSpace(workspaceID)
	taskScopeID = strings.TrimSpace(taskScopeID)
	if workspaceID == "" || taskScopeID == "" {
		return ActiveTaskBoardChannel{}, ErrForbidden
	}
	var resolved ActiveTaskBoardChannel
	err := queryer.QueryRow(ctx, `
SELECT t.id,
       t.task_scope_id,
       t.task_number,
       COALESCE(NULLIF(t.title, ''), ''),
       tb.channel_id
FROM tasks t
JOIN scopes s
  ON s.id = t.task_scope_id
 AND s.workspace_id = $1
 AND s.type = 'task'
 AND s.status = 'active'
 AND s.archived_at IS NULL
JOIN task_boards tb
  ON tb.id = t.board_id
 AND tb.workspace_id = $1
JOIN scopes channel_scope
  ON channel_scope.id = tb.channel_id
 AND channel_scope.workspace_id = $1
 AND channel_scope.type = 'channel'
 AND channel_scope.status = 'active'
 AND channel_scope.archived_at IS NULL
WHERE t.task_scope_id = $2
  AND t.channel_id = tb.channel_id
LIMIT 1
`, workspaceID, taskScopeID).Scan(
		&resolved.TaskID,
		&resolved.TaskScopeID,
		&resolved.TaskNumber,
		&resolved.Title,
		&resolved.ChannelID,
	)
	if err != nil {
		if coredb.IsNotFound(err) {
			return ActiveTaskBoardChannel{}, ErrForbidden
		}
		return ActiveTaskBoardChannel{}, fmt.Errorf("resolve active task board channel: %w", err)
	}
	resolved.TaskID = strings.TrimSpace(resolved.TaskID)
	resolved.TaskScopeID = strings.TrimSpace(resolved.TaskScopeID)
	resolved.Title = strings.TrimSpace(resolved.Title)
	resolved.ChannelID = strings.TrimSpace(resolved.ChannelID)
	if resolved.TaskScopeID == "" || resolved.ChannelID == "" {
		return ActiveTaskBoardChannel{}, ErrForbidden
	}
	return resolved, nil
}

// AuthorizeActiveAgentTaskBoardChannel is the single canonical Agent Task-board
// Channel authorizer. It resolves the task's unique Channel from the active
// Task/board authority (tasks.board_id -> task_boards, never a denormalized
// tasks.channel_id alone), then verifies the same-workspace active
// Agent+Machine+Member currently holds active membership on that Channel scope.
// Orphaned/legacy boards, cross-workspace, other-channel, deleted/archived/
// unresolvable, kind-mismatch, and tasks.channel_id mismatch all fail closed
// non-enumerating (one uniform forbidden result).
func AuthorizeActiveAgentTaskBoardChannel(ctx context.Context, queryer coredb.QueryRower, workspaceID string, memberID string, taskScopeID string) (ActiveTaskBoardChannel, error) {
	resolved, err := ResolveActiveTaskBoardChannel(ctx, queryer, workspaceID, taskScopeID)
	if err != nil {
		return ActiveTaskBoardChannel{}, err
	}
	if err := AuthorizeActiveAgentChannelMembership(ctx, queryer, workspaceID, memberID, resolved.ChannelID); err != nil {
		if errors.Is(err, ErrForbidden) || errors.Is(err, ErrNotFound) {
			return ActiveTaskBoardChannel{}, ErrForbidden
		}
		return ActiveTaskBoardChannel{}, err
	}
	return resolved, nil
}
