package installation

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"reflect"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/app/installation"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

// ShareRequest is the caller-facing share approval request projection.
type ShareRequest struct {
	ID             string
	AppID          string
	AppName        string
	InstallationID string
	Status         string
	Reason         string
	ReviewedPolicy *SharePolicySnapshot
	CreatedAt      time.Time
	UpdatedAt      time.Time
	DecidedAt      *time.Time
}

// RequestShare submits the owner's request to share one personal
// installation with the workspace. The request pins a canonical policy
// snapshot so approval always grants exactly what the approver reviewed.
func (svc *Service) RequestShare(ctx context.Context, userID string, workspaceID string, installationID string, reason string) (ShareRequest, error) {
	workspaceMember, summary, err := svc.requireManagement(ctx, userID, workspaceID, installationID)
	if err != nil {
		return ShareRequest{}, err
	}
	if summary.Installation.OwnershipKind != serviceactor.OwnershipKindPersonal {
		return ShareRequest{}, storeerr.BadRequest("only personal installations can be shared")
	}
	reason = strings.TrimSpace(reason)
	if reason == "" {
		return ShareRequest{}, storeerr.BadRequest("reason is required")
	}

	var requestID string
	err = coredb.WithTx(ctx, svc.db, func(tx coredb.Tx) error {
		if err := installation.LockAppInstallationMutationScope(ctx, tx, summary.Definition.ID); err != nil {
			return err
		}
		if _, err := svc.revalidateManagement(ctx, tx, userID, workspaceID, workspaceMember.Member.ID, summary); err != nil {
			return err
		}
		// Reload the full installation under the lock so the snapshot pins
		// the current version/config, not the pre-lock read.
		lockedSummary, err := svc.actors.InTx(tx).LoadInstallationSummaryByKindForUpdate(ctx, workspaceID, summary.Installation.ID, serviceactor.KindApp)
		if err != nil {
			return err
		}
		if lockedSummary.Installation.Status != serviceactor.StatusActive ||
			lockedSummary.Installation.OwnershipKind != serviceactor.OwnershipKindPersonal {
			return storeerr.Conflict("installation is no longer a shareable personal installation")
		}
		snapshot, err := svc.buildShareSnapshot(ctx, tx, lockedSummary)
		if err != nil {
			return err
		}
		snapshotJSON, err := model.MarshalJSONObject("share request policy snapshot", snapshot)
		if err != nil {
			return err
		}
		scopesJSON, err := model.MarshalJSONObject("share request scopes", serviceactor.NormalizeScopeSet(snapshot.Scopes))
		if err != nil {
			return err
		}
		requestID, err = identifiers.NewEntityID("appreq")
		if err != nil {
			return fmt.Errorf("generate share request id: %w", err)
		}
		return svc.actors.InTx(tx).InsertInstallationShareRequest(ctx, serviceactor.InstallationShareRequest{
			ID:                requestID,
			WorkspaceID:       workspaceID,
			ServiceDefinition: lockedSummary.Definition.ID,
			RequestedVersion:  lockedSummary.Installation.InstalledVersion,
			InstallationID:    lockedSummary.Installation.ID,
			Reason:            reason,
			RequestedScopes:   snapshot.Scopes,
			PolicyDigest:      snapshot.PolicyDigest,
			RequestedBy:       strings.TrimSpace(workspaceMember.Member.ID),
		}, snapshotJSON, scopesJSON, time.Now().UTC())
	})
	if err != nil {
		return ShareRequest{}, err
	}
	return svc.loadShareRequestWithPolicy(ctx, workspaceID, requestID)
}

// DecideShare applies an admin's decision on one pending share request. The
// canonical definition -> member -> request -> installation lock order is
// preserved: a plain preview read locates the definition, the definition
// mutation lock is taken first, and only then are the member, request, and
// installation rows locked.
func (svc *Service) DecideShare(ctx context.Context, userID string, workspaceID string, requestID string, approve bool, reason string) (ShareRequest, error) {
	// Non-locking admin preflight: an unauthorized caller gets the same
	// rejection before any request or installation existence signal.
	preflightMember, err := svc.loadAuthorizedMember(ctx, svc.db, userID, workspaceID, false)
	if err != nil {
		return ShareRequest{}, err
	}
	if !hasWorkspaceAdminRole(preflightMember) {
		return ShareRequest{}, storeerr.ErrForbidden
	}
	preview, found, err := svc.actors.LoadInstallationShareRequest(ctx, workspaceID, requestID)
	if err != nil {
		return ShareRequest{}, err
	}
	if !found {
		return ShareRequest{}, storeerr.NotFound("share request not found")
	}
	var definitionID string
	if err := svc.db.QueryRow(ctx, `
SELECT service_definition_id
FROM service_installations
WHERE workspace_id = $1
  AND id = $2
  AND service_kind = 'app'
LIMIT 1
`, workspaceID, strings.TrimSpace(preview.InstallationID)).Scan(&definitionID); err != nil {
		if coredb.IsNotFound(err) {
			return ShareRequest{}, storeerr.Conflict("installation is no longer a shareable personal installation")
		}
		return ShareRequest{}, fmt.Errorf("load share request installation: %w", err)
	}

	deciderMemberID := ""
	finalStatus := ""
	err = coredb.WithTx(ctx, svc.db, func(tx coredb.Tx) error {
		if err := installation.LockAppInstallationMutationScope(ctx, tx, definitionID); err != nil {
			return err
		}
		currentAdmin, err := svc.loadAuthorizedMember(ctx, tx, userID, workspaceID, true)
		if err != nil {
			return err
		}
		if !hasWorkspaceAdminRole(currentAdmin) {
			return storeerr.Forbidden("workspace administrator role required")
		}
		deciderMemberID = strings.TrimSpace(currentAdmin.Member.ID)

		request, ok, err := svc.actors.InTx(tx).LoadInstallationShareRequestForUpdate(ctx, workspaceID, requestID)
		if err != nil {
			return err
		}
		if !ok {
			return storeerr.NotFound("share request not found")
		}
		if request.Status != "pending" {
			return storeerr.Conflict("share request is already decided")
		}
		installationRow, err := svc.actors.InTx(tx).LoadInstallationForUpdate(ctx, request.InstallationID)
		if err != nil {
			return err
		}
		if installationRow.WorkspaceID != workspaceID || installationRow.ServiceKind != serviceactor.KindApp {
			return storeerr.ErrNotFound
		}
		if installationRow.OwnershipKind != serviceactor.OwnershipKindPersonal || installationRow.Status != serviceactor.StatusActive {
			return storeerr.Conflict("installation is no longer a shareable personal installation")
		}

		now := time.Now().UTC()
		if approve {
			if err := svc.approveShareRequestInTx(ctx, tx, workspaceID, request, installationRow, deciderMemberID, now); err != nil {
				return err
			}
		}
		status := "rejected"
		if approve {
			status = "approved"
		}
		if err := svc.actors.InTx(tx).DecideInstallationShareRequest(ctx, requestID, status, deciderMemberID, strings.TrimSpace(reason), now); err != nil {
			return err
		}
		finalStatus = status
		return nil
	})
	if err != nil {
		return ShareRequest{}, err
	}
	request, err := svc.loadShareRequestWithPolicy(ctx, workspaceID, requestID)
	if err != nil {
		return ShareRequest{}, err
	}
	request.Status = finalStatus
	return request, nil
}

func (svc *Service) loadShareRequestWithPolicy(ctx context.Context, workspaceID string, requestID string) (ShareRequest, error) {
	row, found, err := svc.actors.LoadInstallationShareRequest(ctx, workspaceID, requestID)
	if err != nil {
		return ShareRequest{}, err
	}
	if !found {
		return ShareRequest{}, storeerr.NotFound("share request not found")
	}
	request := toShareRequestProjection(row)
	var appName string
	if err := svc.db.QueryRow(ctx, `
SELECT name FROM service_definitions WHERE id = $1 LIMIT 1
`, row.ServiceDefinition).Scan(&appName); err != nil {
		return ShareRequest{}, fmt.Errorf("load share request app name: %w", err)
	}
	request.AppName = appName
	var snapshotJSON []byte
	if err := svc.db.QueryRow(ctx, `
SELECT requested_config
FROM service_installation_requests
WHERE id = $1
`, requestID).Scan(&snapshotJSON); err != nil {
		return ShareRequest{}, fmt.Errorf("load share request snapshot: %w", err)
	}
	if len(snapshotJSON) > 0 {
		var snapshot SharePolicySnapshot
		if err := model.UnmarshalJSONObject("share request policy snapshot", snapshotJSON, &snapshot); err != nil {
			return ShareRequest{}, fmt.Errorf("decode share request snapshot: %w", err)
		}
		if err := validateShareSnapshotCompleteness(&snapshot, row.ServiceDefinition, row.RequestedVersion); err != nil {
			return ShareRequest{}, err
		}
		request.ReviewedPolicy = &snapshot
	}
	return request, nil
}

// DecodeSharePolicySnapshot strictly decodes and completeness-checks one
// persisted share snapshot. Corrupt or incomplete snapshots fail closed.
func DecodeSharePolicySnapshot(snapshotJSON []byte) (*SharePolicySnapshot, error) {
	if len(snapshotJSON) == 0 {
		return nil, nil
	}
	var snapshot SharePolicySnapshot
	if err := model.UnmarshalJSONObject("share request policy snapshot", snapshotJSON, &snapshot); err != nil {
		return nil, fmt.Errorf("decode share request snapshot: %w", err)
	}
	if err := validateShareSnapshotCompleteness(&snapshot, snapshot.DefinitionID, snapshot.Version); err != nil {
		return nil, err
	}
	return &snapshot, nil
}

// SharePolicySnapshot is the canonical reviewed policy of one personal
// installation at snapshot time: the App version, active scopes,
// installation config, active connection identities (never credentials),
// and the actual Extension selections.
type SharePolicySnapshot struct {
	DefinitionID       string                    `json:"definitionId"`
	Version            string                    `json:"version"`
	Scopes             []string                  `json:"scopes"`
	PolicyDigest       string                    `json:"policyDigest"`
	InstallationConfig map[string]any            `json:"installationConfig"`
	Connections        []ShareConnectionSnapshot `json:"connections"`
	Extensions         []ShareExtensionSnapshot  `json:"extensions"`
}

type ShareConnectionSnapshot struct {
	ID                 string `json:"id"`
	ExternalSubjectRef string `json:"externalSubjectRef"`
	DisplayLabel       string `json:"displayLabel"`
	Status             string `json:"status"`
}

type ShareExtensionSnapshot struct {
	Name         string `json:"name"`
	Domain       string `json:"domain"`
	Version      int64  `json:"version"`
	Digest       string `json:"digest"`
	Status       string `json:"status"`
	DefinitionID string `json:"definitionId"`
}

// buildShareSnapshot locks the installation's Extension and connection rows
// and renders the canonical share policy. Callers hold the App-definition
// mutation-scope lock.
func (svc *Service) buildShareSnapshot(
	ctx context.Context,
	tx coredb.Tx,
	summary serviceactor.InstallationSummary,
) (SharePolicySnapshot, error) {
	target := summary.Installation
	snapshot := SharePolicySnapshot{
		DefinitionID: summary.Definition.ID,
		Version:      target.InstalledVersion,
	}
	installationConfig, err := decodeInstallationConfigStrict(target.Config)
	if err != nil {
		return SharePolicySnapshot{}, err
	}
	snapshot.InstallationConfig = installationConfig
	snapshot.Connections = []ShareConnectionSnapshot{}
	snapshot.Extensions = []ShareExtensionSnapshot{}

	extensionRows, err := tx.Query(ctx, `
SELECT extension_name, definition_domain, definition_version, definition_digest, status
FROM service_installation_extensions
WHERE workspace_id = $1
  AND service_installation_id = $2
ORDER BY definition_domain ASC, extension_name ASC
FOR UPDATE
`, target.WorkspaceID, target.ID)
	if err != nil {
		return SharePolicySnapshot{}, fmt.Errorf("lock share policy extensions: %w", err)
	}
	for extensionRows.Next() {
		var entry ShareExtensionSnapshot
		if err := extensionRows.Scan(&entry.Name, &entry.Domain, &entry.Version, &entry.Digest, &entry.Status); err != nil {
			extensionRows.Close()
			return SharePolicySnapshot{}, fmt.Errorf("scan share policy extension: %w", err)
		}
		entry.DefinitionID = summary.Definition.ID
		snapshot.Extensions = append(snapshot.Extensions, entry)
	}
	if err := extensionRows.Err(); err != nil {
		extensionRows.Close()
		return SharePolicySnapshot{}, fmt.Errorf("iterate share policy extensions: %w", err)
	}
	extensionRows.Close()

	connectionRows, err := tx.Query(ctx, `
SELECT id, external_subject_ref, display_label, status
FROM service_connections
WHERE workspace_id = $1
  AND service_installation_id = $2
  AND status = 'active'
ORDER BY id ASC
FOR UPDATE
`, target.WorkspaceID, target.ID)
	if err != nil {
		return SharePolicySnapshot{}, fmt.Errorf("lock share policy connections: %w", err)
	}
	for connectionRows.Next() {
		var entry ShareConnectionSnapshot
		if err := connectionRows.Scan(&entry.ID, &entry.ExternalSubjectRef, &entry.DisplayLabel, &entry.Status); err != nil {
			connectionRows.Close()
			return SharePolicySnapshot{}, fmt.Errorf("scan share policy connection: %w", err)
		}
		snapshot.Connections = append(snapshot.Connections, entry)
	}
	if err := connectionRows.Err(); err != nil {
		connectionRows.Close()
		return SharePolicySnapshot{}, fmt.Errorf("iterate share policy connections: %w", err)
	}
	connectionRows.Close()

	grants, err := svc.actors.InTx(tx).ListActiveGrantScopesByInstallationIDsForUpdate(ctx, []string{target.ID})
	if err != nil {
		return SharePolicySnapshot{}, err
	}
	snapshot.Scopes = serviceactor.NormalizeScopeSet(grants[target.ID])

	digest, err := shareSnapshotDigest(snapshot)
	if err != nil {
		return SharePolicySnapshot{}, err
	}
	snapshot.PolicyDigest = digest
	return snapshot, nil
}

// shareSnapshotDigest renders the canonical digest of a snapshot with its
// digest field cleared: the pinned string is always derived from the exact
// persisted fields, so a tampered snapshot cannot keep a stale digest.
func shareSnapshotDigest(snapshot SharePolicySnapshot) (string, error) {
	snapshot.PolicyDigest = ""
	canonical, err := model.MarshalJSONObject("share policy digest", snapshot)
	if err != nil {
		return "", err
	}
	sum := sha256.Sum256(canonical)
	return "sha256:" + hex.EncodeToString(sum[:]), nil
}

func decodeJSONObjectMap(raw []byte) map[string]any {
	if len(raw) == 0 {
		return nil
	}
	var decoded map[string]any
	if err := model.UnmarshalJSONObject("installation config", raw, &decoded); err != nil {
		return map[string]any{}
	}
	return decoded
}

// decodeInstallationConfigStrict fails closed for snapshot assembly.
func decodeInstallationConfigStrict(raw []byte) (map[string]any, error) {
	if len(raw) == 0 {
		return map[string]any{}, nil
	}
	var decoded map[string]any
	if err := model.UnmarshalJSONObject("installation config", raw, &decoded); err != nil {
		return nil, err
	}
	return decoded, nil
}

func toShareRequestProjection(row serviceactor.InstallationShareRequest) ShareRequest {
	return ShareRequest{
		ID:             row.ID,
		AppID:          row.ServiceDefinition,
		InstallationID: row.InstallationID,
		Status:         row.Status,
		Reason:         row.Reason,
		CreatedAt:      row.CreatedAt,
		UpdatedAt:      row.UpdatedAt,
		DecidedAt:      row.DecidedAt,
	}
}

// approveShareRequestInTx verifies the pinned policy digest against the
// current snapshot and creates the workspace use authorization for one
// approved share request.
func (svc *Service) approveShareRequestInTx(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	request serviceactor.InstallationShareRequest,
	installationRow serviceactor.Installation,
	deciderMemberID string,
	now time.Time,
) error {
	summary, err := svc.actors.InTx(tx).LoadInstallationSummaryByKindForUpdate(ctx, workspaceID, request.InstallationID, serviceactor.KindApp)
	if err != nil {
		return err
	}
	if strings.TrimSpace(request.PolicyDigest) == "" {
		return storeerr.Conflict("App policy changed since the share request; the owner must resubmit")
	}
	// The pinned digest must be reproduced from the exact persisted
	// snapshot fields: a tampered snapshot keeping a stale digest string,
	// or drifted requested scopes, keeps the request pending.
	var persisted SharePolicySnapshot
	if err := model.UnmarshalJSONObject("share request policy snapshot", request.ReviewedSnapshotJSON, &persisted); err != nil {
		return storeerr.Conflict("App policy changed since the share request; the owner must resubmit")
	}
	persistedDigest, err := shareSnapshotDigest(persisted)
	if err != nil {
		return err
	}
	if persistedDigest != strings.TrimSpace(request.PolicyDigest) ||
		!reflect.DeepEqual(serviceactor.NormalizeScopeSet(persisted.Scopes), serviceactor.NormalizeScopeSet(request.RequestedScopes)) {
		return storeerr.Conflict("App policy changed since the share request; the owner must resubmit")
	}
	snapshot, err := svc.buildShareSnapshot(ctx, tx, summary)
	if err != nil {
		return err
	}
	if strings.TrimSpace(request.PolicyDigest) != snapshot.PolicyDigest {
		return storeerr.Conflict("App policy changed since the share request; the owner must resubmit")
	}
	_, err = svc.actors.InTx(tx).UpsertPrincipalAuthorization(ctx, serviceactor.PrincipalAuthorizationUpsert{
		WorkspaceID:            workspaceID,
		ServiceDefinitionID:    installationRow.ServiceDefinitionID,
		ServiceInstallationID:  request.InstallationID,
		ServiceKind:            serviceactor.KindApp,
		PrincipalKind:          serviceactor.PrincipalKindWorkspace,
		PrincipalRef:           workspaceID,
		AccessRole:             serviceactor.AccessRoleUse,
		Scopes:                 snapshot.Scopes,
		AuthorizedByMemberID:   deciderMemberID,
		Now:                    now,
		ActiveAuthorizationKey: serviceactor.PrincipalAuthorizationKeyDigest(workspaceID, serviceactor.PrincipalKindWorkspace, workspaceID, request.InstallationID),
	})
	if err != nil {
		return err
	}
	if svc.hooks.WorkspaceReconcile != nil {
		if err := svc.hooks.WorkspaceReconcile(ctx, tx, workspaceID, now); err != nil {
			return err
		}
	}
	return nil
}

// validateShareSnapshotCompleteness fails closed on an incomplete or
// mismatched reviewed policy projection.
func validateShareSnapshotCompleteness(snapshot *SharePolicySnapshot, definitionID string, version string) error {
	if snapshot == nil {
		return fmt.Errorf("share request policy snapshot is missing")
	}
	if strings.TrimSpace(snapshot.PolicyDigest) == "" {
		return fmt.Errorf("share request policy snapshot has no digest")
	}
	if strings.TrimSpace(snapshot.DefinitionID) != strings.TrimSpace(definitionID) {
		return fmt.Errorf("share request policy snapshot definition mismatch")
	}
	if strings.TrimSpace(snapshot.Version) != strings.TrimSpace(version) {
		return fmt.Errorf("share request policy snapshot version mismatch")
	}
	if snapshot.InstallationConfig == nil {
		return fmt.Errorf("share request policy snapshot has no installation config")
	}
	if snapshot.Connections == nil {
		return fmt.Errorf("share request policy snapshot has no connections projection")
	}
	if snapshot.Extensions == nil {
		return fmt.Errorf("share request policy snapshot has no extensions projection")
	}
	return nil
}
