package serviceaccount

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"slices"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/access"
	"github.com/tidbcloud/AgentTeam/internal/store/approval"
	serviceaccountstore "github.com/tidbcloud/AgentTeam/internal/store/serviceaccount"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

const approvalSubjectServiceAccountCreateRequest = "service_account_create_request"

// WorkspaceCreatePayload is the immutable business payload bound to one create
// request and, when required, to one Approval subject. IDs are canonical Human
// member IDs rather than user IDs so later Approval/materialization consumers
// do not reinterpret identity.
type WorkspaceCreatePayload struct {
	Name          string `json:"name"`
	Description   string `json:"description,omitempty"`
	AvatarAssetID string `json:"avatarAssetId,omitempty"`
	AvatarURL     string `json:"avatarUrl,omitempty"`
}

// SubmitWorkspaceCreateRequest is the authenticated Human command. The
// request ID and local account ID are optional caller-provided deterministic
// refs used by tests/recovery; production callers normally omit both.
type SubmitWorkspaceCreateRequest struct {
	RequestID                 string
	AccountID                 string
	WorkspaceID               string
	RequesterUserID           string
	ClientRequestID           string
	ExpectedAuthorizationPath string
	Payload                   WorkspaceCreatePayload
	CorrelationID             string
	Now                       time.Time
}

// WorkspaceCreateRequestResult is the authoritative request plus optional
// Approval/account facts committed by the same transaction.
type WorkspaceCreateRequestResult struct {
	Request     serviceaccountstore.CreateRequest
	Approval    *approval.Instance
	Assignments []approval.Assignment
	Account     *serviceaccountstore.Account
	Created     bool
}

// SubmitWorkspaceCreateRequest atomically records the immutable request and
// either materializes the account for an owner/admin or opens an Approval
// subject assigned to all current workspace owners/admins.
func (service *Service) SubmitWorkspaceCreateRequest(ctx context.Context, input SubmitWorkspaceCreateRequest) (WorkspaceCreateRequestResult, error) {
	if err := service.validate(); err != nil {
		return WorkspaceCreateRequestResult{}, err
	}
	prepared, err := prepareWorkspaceCreateRequest(input)
	if err != nil {
		return WorkspaceCreateRequestResult{}, err
	}
	var result WorkspaceCreateRequestResult
	err = coredb.WithTx(ctx, service.db, func(tx coredb.Tx) error {
		requester, err := access.LoadHumanWorkspaceMemberForUpdate(ctx, tx, prepared.RequesterUserID, prepared.WorkspaceID)
		if err != nil {
			return storeerr.Forbidden("service account creation requires an active workspace Human")
		}
		requestStore := service.accounts.InTx(tx)
		existing, err := requestStore.LoadCreateRequestByClientKeyForUpdate(
			ctx, prepared.WorkspaceID, requester.Member.ID, prepared.ClientRequestID,
		)
		switch {
		case err == nil:
			if requester.Member.ID != existing.RequestedByMemberID {
				return storeerr.Forbidden("service account creation requires the same active workspace Human")
			}
			return service.replayWorkspaceCreateRequestInTx(ctx, tx, prepared, existing, &result)
		case !errors.Is(err, serviceaccountstore.ErrNotFound):
			return err
		}

		path := serviceaccountstore.AuthorizationPathApprovalRequired
		if access.HasWorkspaceAdminRole(domain.WorkspaceRole(requester.WorkspaceRole)) {
			path = serviceaccountstore.AuthorizationPathDirectCreate
		}
		if err := requireExpectedWorkspaceCreateAuthorizationPath(prepared.ExpectedAuthorizationPath, path); err != nil {
			return err
		}

		request, err := requestStore.InsertCreateRequest(ctx, serviceaccountstore.CreateRequestInsert{
			ID:                  prepared.RequestID,
			WorkspaceID:         prepared.WorkspaceID,
			RequestedByMemberID: requester.Member.ID,
			ClientRequestID:     prepared.ClientRequestID,
			PayloadSnapshot:     prepared.PayloadSnapshot,
			PayloadDigest:       prepared.PayloadDigest,
			AuthorizationPath:   path,
			Now:                 prepared.Now,
		})
		if err != nil {
			return err
		}
		result.Request = request

		if path == serviceaccountstore.AuthorizationPathDirectCreate {
			accountID := prepared.AccountID
			if accountID == "" {
				accountID, err = identifiers.NewEntityID("sacct")
				if err != nil {
					return fmt.Errorf("generate workspace service account id: %w", err)
				}
			}
			materialization, err := prepareWorkspaceMaterialization(WorkspaceMaterialization{
				Identity:          IdentityRefs{AccountID: accountID},
				WorkspaceID:       prepared.WorkspaceID,
				CreatorMemberID:   requester.Member.ID,
				Name:              prepared.Payload.Name,
				Description:       prepared.Payload.Description,
				AvatarAssetID:     prepared.Payload.AvatarAssetID,
				AvatarURL:         prepared.Payload.AvatarURL,
				ManagerMemberIDs:  []string{requester.Member.ID},
				AuthorizationPath: path,
				CorrelationID:     prepared.CorrelationID,
				Now:               prepared.Now,
			})
			if err != nil {
				return err
			}
			materialized, err := service.materializeWorkspaceInTx(ctx, tx, materialization)
			if err != nil {
				return err
			}
			request, err = requestStore.TransitionCreateRequest(
				ctx, request.WorkspaceID, request.ID, request.Version,
				serviceaccountstore.RequestStateProvisioning, "", 0,
				materialized.Account.ID, prepared.Now,
			)
			if err != nil {
				return err
			}
			result.Request = request
			result.Account = &materialized.Account
			result.Created = materialized.Created
			return nil
		}

		approvers, err := listWorkspaceApproversForUpdate(ctx, tx, prepared.WorkspaceID)
		if err != nil {
			return err
		}
		approvalID := deterministicApprovalID(request.ID)
		instance, assignments, created, err := approval.New(service.db).InTx(tx).Create(ctx, approval.CreateParams{
			ID:                approvalID,
			WorkspaceID:       request.WorkspaceID,
			SubjectType:       approvalSubjectServiceAccountCreateRequest,
			SubjectRef:        request.ID,
			SubjectVersion:    request.RequestVersion,
			PayloadDigest:     request.PayloadDigest,
			ApplicantMemberID: request.RequestedByMemberID,
			ApproverMemberIDs: approvers,
			CorrelationID:     prepared.CorrelationID,
			Now:               prepared.Now,
		})
		if err != nil {
			return err
		}
		request, err = requestStore.TransitionCreateRequest(
			ctx, request.WorkspaceID, request.ID, request.Version,
			serviceaccountstore.RequestStateAwaitingAuthorization,
			instance.ID, 0, "", prepared.Now,
		)
		if err != nil {
			return err
		}
		result.Request = request
		result.Approval = &instance
		result.Assignments = assignments
		result.Created = created
		return nil
	})
	if err == nil && IsPrivilegedWorkspaceCreateCancellation(result.Request) {
		// The obsolete Approval cleanup must commit, but a replay cannot masquerade
		// as a successful direct create. The caller must submit a fresh direct
		// command with a new client key.
		return result, storeerr.Conflict("obsolete Service Account approval was withdrawn after requester role promotion; submit a new direct-create request")
	}
	return result, err
}

func (service *Service) replayWorkspaceCreateRequestInTx(
	ctx context.Context,
	tx coredb.Tx,
	prepared preparedWorkspaceCreateRequest,
	request serviceaccountstore.CreateRequest,
	result *WorkspaceCreateRequestResult,
) error {
	if request.PayloadDigest != prepared.PayloadDigest {
		return storeerr.Conflict("service account create request idempotency key was reused with a different payload")
	}
	result.Request = request
	if IsPrivilegedWorkspaceCreateCancellation(request) {
		return nil
	}
	if request.AuthorizationPath == serviceaccountstore.AuthorizationPathApprovalRequired &&
		request.BusinessState == serviceaccountstore.RequestStateAwaitingAuthorization &&
		strings.TrimSpace(request.ApprovalInstanceID) != "" {
		workspaceRole, err := lockWorkspaceCreateRequesterRole(
			ctx, tx, request.WorkspaceID, request.RequestedByMemberID,
		)
		if err != nil {
			return err
		}
		if access.HasWorkspaceAdminRole(domain.WorkspaceRole(workspaceRole)) {
			instance, assignments, err := lockBoundWorkspaceCreateApprovalInTx(ctx, service.db, tx, request)
			if err != nil {
				return err
			}
			request, _, _, err = settleKnownPrivilegedWorkspaceCreateRequestInTx(
				ctx, tx, request, instance, assignments, prepared.Now,
			)
			if err != nil {
				return err
			}
			result.Request = request
			// The obsolete generic Approval is retained only as a durable audit
			// fact. An administrator replay must not surface an approval response.
			result.Approval = nil
			result.Assignments = nil
			return nil
		}
	}
	requestStore := service.accounts.InTx(tx)
	if request.CreatedServiceAccountID != "" {
		account, err := requestStore.LoadAccountForUpdate(ctx, request.WorkspaceID, request.CreatedServiceAccountID)
		if err != nil {
			return err
		}
		result.Account = &account
	}

	switch request.AuthorizationPath {
	case serviceaccountstore.AuthorizationPathDirectCreate:
		// Creation and request transition share one transaction, so a committed
		// direct request without its account ref is not a valid replay state.
		if result.Account == nil {
			return storeerr.Conflict("direct service account create request is missing its materialized account")
		}
		return nil
	case serviceaccountstore.AuthorizationPathApprovalRequired:
	default:
		return storeerr.Conflict("service account create request has an invalid authorization path")
	}
	if request.ApprovalInstanceID == "" {
		return storeerr.Conflict("approval-required service account create request is missing its Approval instance")
	}

	approvalStore := approval.New(service.db).InTx(tx)
	instance, err := approvalStore.Load(ctx, request.WorkspaceID, request.ApprovalInstanceID)
	if err != nil {
		return err
	}
	if instance.SubjectType != approvalSubjectServiceAccountCreateRequest ||
		instance.SubjectRef != request.ID ||
		instance.SubjectVersion != request.RequestVersion ||
		instance.PayloadDigest != request.PayloadDigest ||
		instance.ApplicantMemberID != request.RequestedByMemberID {
		return storeerr.Conflict("Approval immutable subject does not match the service account create request")
	}
	assignments, err := approvalStore.ListAssignments(ctx, request.WorkspaceID, instance.ID)
	if err != nil {
		return err
	}
	result.Approval = &instance
	result.Assignments = assignments
	return nil
}

func deterministicApprovalID(requestID string) string {
	digest := sha256.Sum256([]byte("service-account-create-approval\x00" + strings.TrimSpace(requestID)))
	return "appr_" + hex.EncodeToString(digest[:16])
}

type preparedWorkspaceCreateRequest struct {
	SubmitWorkspaceCreateRequest
	Payload         WorkspaceCreatePayload
	PayloadSnapshot []byte
	PayloadDigest   string
	Now             time.Time
}

func prepareWorkspaceCreateRequest(input SubmitWorkspaceCreateRequest) (preparedWorkspaceCreateRequest, error) {
	input.RequestID = strings.TrimSpace(input.RequestID)
	input.AccountID = strings.TrimSpace(input.AccountID)
	input.WorkspaceID = strings.TrimSpace(input.WorkspaceID)
	input.RequesterUserID = strings.TrimSpace(input.RequesterUserID)
	input.ClientRequestID = strings.TrimSpace(input.ClientRequestID)
	input.ExpectedAuthorizationPath = strings.TrimSpace(input.ExpectedAuthorizationPath)
	input.CorrelationID = strings.TrimSpace(input.CorrelationID)
	if input.WorkspaceID == "" || input.RequesterUserID == "" || input.ClientRequestID == "" {
		return preparedWorkspaceCreateRequest{}, storeerr.BadRequest("workspaceId, requesterUserId, and clientRequestId are required")
	}
	if len(input.ClientRequestID) > 128 {
		return preparedWorkspaceCreateRequest{}, storeerr.BadRequest("clientRequestId must be at most 128 bytes")
	}
	if len(input.CorrelationID) > 255 {
		return preparedWorkspaceCreateRequest{}, storeerr.BadRequest("correlationId must be at most 255 bytes")
	}
	if input.ExpectedAuthorizationPath != "" &&
		input.ExpectedAuthorizationPath != serviceaccountstore.AuthorizationPathDirectCreate &&
		input.ExpectedAuthorizationPath != serviceaccountstore.AuthorizationPathApprovalRequired {
		return preparedWorkspaceCreateRequest{}, storeerr.BadRequest("expectedAuthorizationPath must be direct_create or approval_required")
	}
	if input.RequestID == "" {
		var err error
		input.RequestID, err = identifiers.NewEntityID("sacreq")
		if err != nil {
			return preparedWorkspaceCreateRequest{}, fmt.Errorf("generate service account create request id: %w", err)
		}
	}
	if len(input.RequestID) > 64 || len(input.AccountID) > 64 {
		return preparedWorkspaceCreateRequest{}, storeerr.BadRequest("requestId and accountId must be at most 64 bytes")
	}
	payload, err := normalizeWorkspaceCreatePayload(input.Payload)
	if err != nil {
		return preparedWorkspaceCreateRequest{}, err
	}
	snapshot, err := json.Marshal(payload)
	if err != nil {
		return preparedWorkspaceCreateRequest{}, fmt.Errorf("marshal service account create payload: %w", err)
	}
	digest := sha256.Sum256(snapshot)
	return preparedWorkspaceCreateRequest{
		SubmitWorkspaceCreateRequest: input,
		Payload:                      payload,
		PayloadSnapshot:              snapshot,
		PayloadDigest:                hex.EncodeToString(digest[:]),
		Now:                          normalizedNow(input.Now),
	}, nil
}

// requireExpectedWorkspaceCreateAuthorizationPath is an optimistic authority
// fence supplied by the browser. In particular, a UI that rendered an
// administrator-only direct-create action must never silently degrade that
// action into an Approval after the caller's canonical workspace role changes.
// Empty preserves compatibility for callers that predate this fence; the
// server still derives the actual path exclusively from the canonical role.
func requireExpectedWorkspaceCreateAuthorizationPath(expected, actual string) error {
	expected = strings.TrimSpace(expected)
	actual = strings.TrimSpace(actual)
	if expected == "" || expected == actual {
		return nil
	}
	if expected == serviceaccountstore.AuthorizationPathDirectCreate {
		return storeerr.Forbidden("workspace administrator authority is required for direct service account creation")
	}
	return storeerr.Conflict("workspace role changed; refresh before creating the service account")
}

func normalizeWorkspaceCreatePayload(payload WorkspaceCreatePayload) (WorkspaceCreatePayload, error) {
	name, err := serviceaccountstore.NormalizeName(payload.Name)
	if err != nil {
		return WorkspaceCreatePayload{}, err
	}
	payload.Name = name
	payload.Description = strings.TrimSpace(payload.Description)
	payload.AvatarAssetID = strings.TrimSpace(payload.AvatarAssetID)
	payload.AvatarURL = strings.TrimSpace(payload.AvatarURL)
	return payload, nil
}

func listWorkspaceApproversForUpdate(ctx context.Context, tx coredb.Tx, workspaceID string) ([]string, error) {
	rows, err := tx.Query(ctx, `
SELECT id
FROM members
WHERE workspace_id = $1
  AND type = 'human'
  AND status = 'active'
  AND workspace_role IN ('owner', 'admin')
ORDER BY id ASC
FOR UPDATE
`, strings.TrimSpace(workspaceID))
	if err != nil {
		return nil, fmt.Errorf("list workspace service account approvers: %w", err)
	}
	defer rows.Close()
	approvers := make([]string, 0)
	for rows.Next() {
		var memberID string
		if err := rows.Scan(&memberID); err != nil {
			return nil, fmt.Errorf("scan workspace service account approver: %w", err)
		}
		if memberID = strings.TrimSpace(memberID); memberID != "" {
			approvers = append(approvers, memberID)
		}
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate workspace service account approvers: %w", err)
	}
	slices.Sort(approvers)
	approvers = slices.Compact(approvers)
	if len(approvers) == 0 {
		return nil, storeerr.Conflict("workspace service account approval requires an active owner or admin")
	}
	return approvers, nil
}
