package approvalapp

import (
	"context"
	"database/sql"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/approval"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

// Resolver validates Approval-owned recipient facts. Assignment facts are
// active only while pending; App-installation reviewer facts additionally stay
// active during a marked setup_patching recovery, where Approve is the only
// legal action. Applicant facts remain addressable at the business-object
// version matching each lifecycle phase so pending receipts and terminal
// results cannot cross their request-state boundary. The durable
// approval_required marker keeps internal provider sagas out of every Approval
// view even though they share the request table and some status values.
type Resolver struct{}

func (Resolver) ResolveAppNotificationRecipient(
	ctx context.Context,
	queryer coredb.QueryRower,
	request app.AppNotificationRecipientFactRequest,
) (app.AppNotificationRecipientFact, error) {
	switch strings.TrimSpace(request.Basis.Kind) {
	case RecipientBasisAssignment:
		return resolveAssignmentFact(ctx, queryer, request)
	case RecipientBasisApplicant:
		return resolveApplicantFact(ctx, queryer, request)
	case RecipientBasisAppInstallationReviewer:
		return resolveAppInstallationReviewerFact(ctx, queryer, request)
	case RecipientBasisAppInstallationApplicant:
		return resolveAppInstallationApplicantFact(ctx, queryer, request)
	case RecipientBasisWikiGenerationRequester:
		return resolveWikiGenerationRequesterFact(ctx, queryer, request)
	case RecipientBasisWorkflowRunAssignee:
		return resolveWorkflowRunAssigneeFact(ctx, queryer, request)
	default:
		return app.AppNotificationRecipientFact{}, storeerr.NotFound("Approval recipient fact not found")
	}
}

func resolveWikiGenerationRequesterFact(
	ctx context.Context,
	queryer coredb.QueryRower,
	request app.AppNotificationRecipientFactRequest,
) (app.AppNotificationRecipientFact, error) {
	requesterID := strings.TrimSpace(request.Basis.Ref)
	requestID := strings.TrimSpace(request.BusinessObject.ID)
	if strings.TrimSpace(request.BusinessObject.Namespace) != BusinessNamespace ||
		strings.TrimSpace(request.BusinessObject.Type) != BusinessTypeWikiGenerationRequest ||
		requestID == "" || request.BusinessObject.Version <= 0 || requesterID == "" {
		return app.AppNotificationRecipientFact{}, storeerr.Forbidden("Wiki generation requester business object is invalid")
	}
	var workspaceID, memberID, memberType, memberStatus string
	err := queryer.QueryRow(ctx, `
SELECT workspace_id, id, type, status
FROM members
WHERE workspace_id = $1 AND user_id = $2
LIMIT 1
FOR UPDATE
`, strings.TrimSpace(request.WorkspaceID), requesterID).Scan(&workspaceID, &memberID, &memberType, &memberStatus)
	if err != nil {
		if coredb.IsNotFound(err) {
			return app.AppNotificationRecipientFact{}, storeerr.NotFound("Wiki generation requester recipient not found")
		}
		return app.AppNotificationRecipientFact{}, fmt.Errorf("resolve Wiki generation requester recipient: %w", err)
	}
	if expected := strings.TrimSpace(request.ExpectedMemberID); expected != "" && expected != memberID {
		return app.AppNotificationRecipientFact{}, storeerr.Forbidden("Wiki generation requester recipient does not match")
	}
	return app.AppNotificationRecipientFact{
		Kind: RecipientBasisWikiGenerationRequester, Ref: requesterID,
		WorkspaceID: workspaceID, RecipientMemberID: memberID,
		Version: request.BusinessObject.Version, Status: "terminal",
		Active:             memberType == "human" && memberStatus == "active",
		PermissionBasisRef: requestID + ":" + memberID,
	}, nil
}

func (Resolver) AuthorizeAppNotificationAction(
	ctx context.Context,
	queryer coredb.QueryRower,
	request app.AppNotificationActionAuthorizationRequest,
) error {
	basisKind := strings.TrimSpace(request.Basis.Kind)
	if basisKind != RecipientBasisAssignment && basisKind != RecipientBasisAppInstallationReviewer && basisKind != RecipientBasisWorkflowRunAssignee {
		return storeerr.Forbidden("Approval result notifications do not accept actions")
	}
	if request.ActionKey != ActionApprove && request.ActionKey != ActionReject {
		return storeerr.Forbidden("Approval action is not allowed")
	}
	factRequest := app.AppNotificationRecipientFactRequest{
		WorkspaceID: request.WorkspaceID, Basis: request.Basis,
		BusinessObject: request.BusinessObject, ExpectedMemberID: request.ActorMemberID,
		Now: request.Now,
	}
	var fact app.AppNotificationRecipientFact
	var err error
	switch basisKind {
	case RecipientBasisAssignment:
		fact, err = resolveAssignmentFact(ctx, queryer, factRequest)
	case RecipientBasisAppInstallationReviewer:
		fact, err = resolveAppInstallationReviewerFact(ctx, queryer, factRequest)
	case RecipientBasisWorkflowRunAssignee:
		fact, err = resolveWorkflowRunAssigneeFact(ctx, queryer, factRequest)
	default:
		err = storeerr.Forbidden("Approval result notifications do not accept actions")
	}
	if err != nil {
		return err
	}
	if !fact.Active {
		return storeerr.Conflict("Approval assignment is no longer pending")
	}
	if basisKind == RecipientBasisAppInstallationReviewer &&
		strings.TrimSpace(fact.Status) == app.AppInstallationRequestStatusSetupPatching &&
		request.ActionKey == ActionReject {
		return storeerr.Forbidden("GitHub setup recovery only accepts approval retry")
	}
	if basisKind == RecipientBasisWorkflowRunAssignee {
		if _, _, err := workflowRunApprovalComment(request.Input); err != nil {
			return storeerr.BadRequest(err.Error())
		}
		return nil
	}
	if request.ActionKey == ActionApprove && len(request.Input) != 0 {
		return storeerr.BadRequest("Approval approve action does not accept input")
	}
	return nil
}

func resolveAppInstallationReviewerFact(
	ctx context.Context,
	queryer coredb.QueryRower,
	request app.AppNotificationRecipientFactRequest,
) (app.AppNotificationRecipientFact, error) {
	reviewerMemberID := strings.TrimSpace(request.Basis.Ref)
	var requestID, workspaceID, status, role, memberType, memberStatus string
	var approvalRequired bool
	err := queryer.QueryRow(ctx, `
SELECT installation_request.id, installation_request.workspace_id,
       installation_request.status, installation_request.approval_required,
       reviewer.workspace_role,
       reviewer.type, reviewer.status
FROM service_installation_requests installation_request
JOIN members reviewer
  ON reviewer.workspace_id = installation_request.workspace_id
 AND reviewer.id = $3
WHERE installation_request.workspace_id = $1
  AND installation_request.id = $2
LIMIT 1
FOR UPDATE
`, strings.TrimSpace(request.WorkspaceID), strings.TrimSpace(request.BusinessObject.ID), reviewerMemberID).Scan(
		&requestID, &workspaceID, &status, &approvalRequired, &role, &memberType, &memberStatus,
	)
	if err != nil {
		if coredb.IsNotFound(err) {
			return app.AppNotificationRecipientFact{}, storeerr.NotFound("App installation reviewer fact not found")
		}
		return app.AppNotificationRecipientFact{}, fmt.Errorf("resolve App installation reviewer recipient: %w", err)
	}
	version, err := validateAppInstallationBusinessObject(request.BusinessObject, requestID, status, approvalRequired)
	if err != nil {
		return app.AppNotificationRecipientFact{}, err
	}
	expectedMemberID := strings.TrimSpace(request.ExpectedMemberID)
	if expectedMemberID != "" && expectedMemberID != reviewerMemberID {
		return app.AppNotificationRecipientFact{}, storeerr.Forbidden("App installation reviewer recipient does not match")
	}
	activeReviewer := memberType == "human" && memberStatus == "active" && (role == "owner" || role == "admin")
	return app.AppNotificationRecipientFact{
		Kind: RecipientBasisAppInstallationReviewer, Ref: reviewerMemberID,
		WorkspaceID: workspaceID, RecipientMemberID: reviewerMemberID,
		Version: version, Status: status,
		Active:             appInstallationRequestReviewActive(status, approvalRequired) && activeReviewer,
		PermissionBasisRef: requestID + ":" + reviewerMemberID,
	}, nil
}

func resolveAppInstallationApplicantFact(
	ctx context.Context,
	queryer coredb.QueryRower,
	request app.AppNotificationRecipientFactRequest,
) (app.AppNotificationRecipientFact, error) {
	var requestID, workspaceID, status, applicantMemberID string
	var approvalRequired bool
	err := queryer.QueryRow(ctx, `
SELECT id, workspace_id, status, approval_required, requested_by_member_id
FROM service_installation_requests
WHERE workspace_id = $1 AND id = $2
LIMIT 1
FOR UPDATE
`, strings.TrimSpace(request.WorkspaceID), strings.TrimSpace(request.Basis.Ref)).Scan(
		&requestID, &workspaceID, &status, &approvalRequired, &applicantMemberID,
	)
	if err != nil {
		if coredb.IsNotFound(err) {
			return app.AppNotificationRecipientFact{}, storeerr.NotFound("App installation applicant fact not found")
		}
		return app.AppNotificationRecipientFact{}, fmt.Errorf("resolve App installation applicant recipient: %w", err)
	}
	version, err := validateAppInstallationBusinessObject(request.BusinessObject, requestID, status, approvalRequired)
	if err != nil {
		return app.AppNotificationRecipientFact{}, err
	}
	expectedMemberID := strings.TrimSpace(request.ExpectedMemberID)
	if expectedMemberID != "" && expectedMemberID != applicantMemberID {
		return app.AppNotificationRecipientFact{}, storeerr.Forbidden("App installation applicant recipient does not match")
	}
	return app.AppNotificationRecipientFact{
		Kind: RecipientBasisAppInstallationApplicant, Ref: requestID,
		WorkspaceID: workspaceID, RecipientMemberID: applicantMemberID,
		Version: version, Status: status,
		// App applicant notifications exist at two distinct business
		// versions: a pending receipt at v1 and a terminal result at v2.
		// Requiring the requested version to match the durable request closes
		// the decision-vs-pending-projection race: a delayed v1 create cannot
		// append a stale "submitted" receipt after the request is decided.
		// Mutation authorization substitutes its requested next version, so an
		// existing v1 receipt can still converge to v2 after the decision.
		Active:             request.BusinessObject.Version == version,
		PermissionBasisRef: requestID,
	}, nil
}

func validateAppInstallationBusinessObject(
	object app.AppNotificationBusinessObject,
	requestID string,
	status string,
	approvalRequired bool,
) (int64, error) {
	if strings.TrimSpace(object.Namespace) != BusinessNamespace ||
		strings.TrimSpace(object.Type) != BusinessTypeAppInstallationRequest ||
		strings.TrimSpace(object.ID) != strings.TrimSpace(requestID) {
		return 0, storeerr.Forbidden("App installation business object does not match recipient fact")
	}
	if !approvalRequired {
		return 0, storeerr.Forbidden("App installation request is not human-reviewable")
	}
	version := int64(1)
	if !appInstallationRequestReviewActive(status, approvalRequired) {
		version = 2
	}
	if object.Version <= 0 || object.Version > version {
		return 0, storeerr.Conflict("App installation business object version is invalid")
	}
	return version, nil
}

func resolveAssignmentFact(
	ctx context.Context,
	queryer coredb.QueryRower,
	request app.AppNotificationRecipientFactRequest,
) (app.AppNotificationRecipientFact, error) {
	var assignment approval.Assignment
	var instance approval.Instance
	var decidedAt, assignmentClosedAt sql.NullTime
	err := queryer.QueryRow(ctx, `
SELECT assignment.id, assignment.workspace_id, assignment.approval_instance_id,
       assignment.approver_member_id, assignment.status, assignment.version,
       assignment.created_at, assignment.updated_at, assignment.closed_at,
       instance.id, instance.workspace_id, instance.subject_type,
       instance.subject_ref, instance.subject_version, instance.payload_digest,
       instance.applicant_member_id, instance.state, instance.decision,
       instance.decision_reason, COALESCE(instance.decided_by_member_id, ''),
       instance.version, instance.correlation_id, instance.created_at,
       instance.updated_at, instance.decided_at
FROM approval_assignments assignment
JOIN approval_instances instance
  ON instance.id = assignment.approval_instance_id
 AND instance.workspace_id = assignment.workspace_id
WHERE assignment.workspace_id = $1
  AND assignment.id = $2
LIMIT 1
`, strings.TrimSpace(request.WorkspaceID), strings.TrimSpace(request.Basis.Ref)).Scan(
		&assignment.ID, &assignment.WorkspaceID, &assignment.ApprovalInstanceID,
		&assignment.ApproverMemberID, &assignment.Status, &assignment.Version,
		&assignment.CreatedAt, &assignment.UpdatedAt, &assignmentClosedAt,
		&instance.ID, &instance.WorkspaceID, &instance.SubjectType,
		&instance.SubjectRef, &instance.SubjectVersion, &instance.PayloadDigest,
		&instance.ApplicantMemberID, &instance.State, &instance.Decision,
		&instance.DecisionReason, &instance.DecidedByMemberID,
		&instance.Version, &instance.CorrelationID, &instance.CreatedAt,
		&instance.UpdatedAt, &decidedAt,
	)
	if err != nil {
		if coredb.IsNotFound(err) {
			return app.AppNotificationRecipientFact{}, storeerr.NotFound("Approval assignment not found")
		}
		return app.AppNotificationRecipientFact{}, fmt.Errorf("resolve Approval assignment recipient: %w", err)
	}
	assignment.ClosedAt = nullableTime(assignmentClosedAt)
	instance.DecidedAt = nullableTime(decidedAt)
	if err := validateApprovalBusinessObject(request.BusinessObject, instance); err != nil {
		return app.AppNotificationRecipientFact{}, err
	}
	expectedMemberID := strings.TrimSpace(request.ExpectedMemberID)
	if expectedMemberID != "" && expectedMemberID != assignment.ApproverMemberID {
		return app.AppNotificationRecipientFact{}, storeerr.Forbidden("Approval assignment recipient does not match")
	}
	return app.AppNotificationRecipientFact{
		Kind: RecipientBasisAssignment, Ref: assignment.ID,
		WorkspaceID: assignment.WorkspaceID, RecipientMemberID: assignment.ApproverMemberID,
		Version: assignment.Version, Status: assignment.Status,
		Active:             assignment.Status == approval.AssignmentPending && instance.State == approval.StatePending,
		PermissionBasisRef: assignment.ID,
	}, nil
}

func resolveApplicantFact(
	ctx context.Context,
	queryer coredb.QueryRower,
	request app.AppNotificationRecipientFactRequest,
) (app.AppNotificationRecipientFact, error) {
	var instance approval.Instance
	var decidedAt sql.NullTime
	err := queryer.QueryRow(ctx, `
SELECT id, workspace_id, subject_type, subject_ref, subject_version,
       payload_digest, applicant_member_id, state, decision, decision_reason,
       COALESCE(decided_by_member_id, ''), version, correlation_id,
       created_at, updated_at, decided_at
FROM approval_instances
WHERE workspace_id = $1 AND id = $2
LIMIT 1
`, strings.TrimSpace(request.WorkspaceID), strings.TrimSpace(request.Basis.Ref)).Scan(
		&instance.ID, &instance.WorkspaceID, &instance.SubjectType, &instance.SubjectRef,
		&instance.SubjectVersion, &instance.PayloadDigest, &instance.ApplicantMemberID,
		&instance.State, &instance.Decision, &instance.DecisionReason,
		&instance.DecidedByMemberID, &instance.Version, &instance.CorrelationID,
		&instance.CreatedAt, &instance.UpdatedAt, &decidedAt,
	)
	if err != nil {
		if coredb.IsNotFound(err) {
			return app.AppNotificationRecipientFact{}, storeerr.NotFound("Approval applicant fact not found")
		}
		return app.AppNotificationRecipientFact{}, fmt.Errorf("resolve Approval applicant recipient: %w", err)
	}
	instance.DecidedAt = nullableTime(decidedAt)
	if err := validateApprovalBusinessObject(request.BusinessObject, instance); err != nil {
		return app.AppNotificationRecipientFact{}, err
	}
	expectedMemberID := strings.TrimSpace(request.ExpectedMemberID)
	if expectedMemberID != "" && expectedMemberID != instance.ApplicantMemberID {
		return app.AppNotificationRecipientFact{}, storeerr.Forbidden("Approval applicant recipient does not match")
	}
	return app.AppNotificationRecipientFact{
		Kind: RecipientBasisApplicant, Ref: instance.ID,
		WorkspaceID: instance.WorkspaceID, RecipientMemberID: instance.ApplicantMemberID,
		Version: instance.Version, Status: instance.State, Active: true,
		PermissionBasisRef: instance.ID,
	}, nil
}

func nullableTime(value sql.NullTime) *time.Time {
	if !value.Valid {
		return nil
	}
	normalized := value.Time.UTC()
	return &normalized
}

func validateApprovalBusinessObject(object app.AppNotificationBusinessObject, instance approval.Instance) error {
	if strings.TrimSpace(object.Namespace) != BusinessNamespace ||
		strings.TrimSpace(object.Type) != BusinessTypeInstance ||
		strings.TrimSpace(object.ID) != instance.ID {
		return storeerr.Forbidden("Approval business object does not match recipient fact")
	}
	if object.Version <= 0 || object.Version > instance.Version {
		return storeerr.Conflict("Approval business object version is invalid")
	}
	return nil
}
