package content

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

var ErrAuthorityConflict = errors.New("content authority conflict")

const MaxAuthorityBindingBatch = 2000

const legacyAuthorityFencePrefix = "legacy-fence:v1|"

// ActivateNativeAuthorityParams identifies one exact product binding. A zero
// expected generation means the binding is still implicit legacy and has no
// persisted row. Activation is intentionally one way: once native, a binding
// is never routed back to AGS.
type ActivateNativeAuthorityParams struct {
	WorkspaceID        string
	Domain             string
	BindingKey         string
	ExpectedGeneration uint64
	ExpectedCheckpoint string
	Checkpoint         string
	Actor              string
	Now                time.Time
}

// LegacyAuthorityFence is a durable, exact-binding mutual exclusion token for
// bounded migration and one-way activation. It coordinates only migration and
// cutover; it never waits for or fences legacy product writers.
type LegacyAuthorityFence struct {
	WorkspaceID string
	Domain      string
	BindingKey  string
	Generation  uint64
	Checkpoint  string
	ExpiresAt   time.Time
}

type AcquireLegacyAuthorityFenceParams struct {
	WorkspaceID string
	Domain      string
	BindingKey  string
	Token       string
	Actor       string
	Now         time.Time
	ExpiresAt   time.Time
}

// AcquireLegacyAuthorityFence materializes an explicit generation-zero legacy
// row only on this write path. The checkpoint is a bounded lease so a crashed
// migration cannot block cutover forever.
func (s *Store) AcquireLegacyAuthorityFence(ctx context.Context, params AcquireLegacyAuthorityFenceParams) (LegacyAuthorityFence, error) {
	if err := s.ensureDB(); err != nil {
		return LegacyAuthorityFence{}, err
	}
	checkpoint, err := normalizeLegacyAuthorityFenceParams(&params)
	if err != nil {
		return LegacyAuthorityFence{}, err
	}
	var result Authority
	err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		id, idErr := newAuthorityID()
		if idErr != nil {
			return idErr
		}
		candidate := Authority{
			ID: id, WorkspaceID: params.WorkspaceID, Domain: params.Domain, BindingKey: params.BindingKey,
			State: AuthorityStateLegacyActive, Generation: 0, Checkpoint: "", UpdatedBy: params.Actor,
			CreatedAt: params.Now, UpdatedAt: params.Now,
		}
		if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&candidate).Error; err != nil {
			return err
		}
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("workspace_id = ? AND domain = ? AND binding_key = ?", params.WorkspaceID, params.Domain, params.BindingKey).
			First(&result).Error; err != nil {
			return err
		}
		if result.State != AuthorityStateLegacyActive || result.Generation != 0 {
			return ErrAuthorityConflict
		}
		if result.Checkpoint != "" {
			currentToken, currentExpiry, parseErr := decodeLegacyAuthorityFence(result.Checkpoint)
			if parseErr != nil || (currentToken != params.Token && currentExpiry.After(params.Now)) {
				return ErrAuthorityConflict
			}
		}
		updated := tx.Model(&Authority{}).
			Where("id = ? AND state = ? AND generation = ? AND checkpoint = ?", result.ID, AuthorityStateLegacyActive, uint64(0), result.Checkpoint).
			Updates(map[string]any{"checkpoint": checkpoint, "updated_by": params.Actor, "updated_at": params.Now})
		if updated.Error != nil {
			return updated.Error
		}
		if updated.RowsAffected != 1 {
			return ErrAuthorityConflict
		}
		result.Checkpoint = checkpoint
		result.UpdatedBy = params.Actor
		result.UpdatedAt = params.Now
		return nil
	})
	if err != nil {
		if errors.Is(err, ErrAuthorityConflict) {
			return LegacyAuthorityFence{}, ErrAuthorityConflict
		}
		return LegacyAuthorityFence{}, fmt.Errorf("acquire legacy content authority fence: %w", err)
	}
	return LegacyAuthorityFence{
		WorkspaceID: result.WorkspaceID, Domain: result.Domain, BindingKey: result.BindingKey,
		Generation: result.Generation, Checkpoint: result.Checkpoint, ExpiresAt: params.ExpiresAt,
	}, nil
}

// ReleaseLegacyAuthorityFence clears only the caller's still-legacy token.
// Native activation replaces the checkpoint and is intentionally a no-op here.
func (s *Store) ReleaseLegacyAuthorityFence(ctx context.Context, fence LegacyAuthorityFence, now time.Time) error {
	if err := s.ensureDB(); err != nil {
		return err
	}
	workspaceID, domain, bindingKey, err := normalizeAuthorityBinding(fence.WorkspaceID, fence.Domain, fence.BindingKey)
	if err != nil || strings.TrimSpace(fence.Checkpoint) == "" || now.IsZero() {
		return fmt.Errorf("legacy content authority fence release is invalid")
	}
	updated := s.db.WithContext(ctx).Model(&Authority{}).
		Where("workspace_id = ? AND domain = ? AND binding_key = ? AND state = ? AND generation = ? AND checkpoint = ?", workspaceID, domain, bindingKey, AuthorityStateLegacyActive, fence.Generation, fence.Checkpoint).
		Updates(map[string]any{"checkpoint": "", "updated_at": now.UTC()})
	if updated.Error != nil {
		return fmt.Errorf("release legacy content authority fence: %w", updated.Error)
	}
	return nil
}

// RequireLegacyAuthorityFence locks and verifies one exact durable fence in an
// existing transaction. Cross-domain projection coordinators use this before
// making the corresponding product projection visible.
func (s *Store) RequireLegacyAuthorityFence(ctx context.Context, fence LegacyAuthorityFence, now time.Time) error {
	if err := s.ensureDB(); err != nil {
		return err
	}
	workspaceID, domain, bindingKey, err := normalizeAuthorityBinding(fence.WorkspaceID, fence.Domain, fence.BindingKey)
	if err != nil || strings.TrimSpace(fence.Checkpoint) == "" || now.IsZero() {
		return ErrAuthorityConflict
	}
	_, expiresAt, err := decodeLegacyAuthorityFence(fence.Checkpoint)
	if err != nil || !expiresAt.After(now.UTC()) {
		return ErrAuthorityConflict
	}
	var authority Authority
	if err := s.db.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).
		Where("workspace_id = ? AND domain = ? AND binding_key = ?", workspaceID, domain, bindingKey).
		First(&authority).Error; err != nil {
		return ErrAuthorityConflict
	}
	if authority.State != AuthorityStateLegacyActive || authority.Generation != fence.Generation || authority.Checkpoint != fence.Checkpoint {
		return ErrAuthorityConflict
	}
	return nil
}

// GetAuthority performs one exact unique-key lookup. Missing rows are returned
// as legacy_active without writing a default row.
func (s *Store) GetAuthority(ctx context.Context, workspaceID, domain, bindingKey string) (Authority, bool, error) {
	if err := s.ensureDB(); err != nil {
		return Authority{}, false, err
	}
	workspaceID, domain, bindingKey, err := normalizeAuthorityBinding(workspaceID, domain, bindingKey)
	if err != nil {
		return Authority{}, false, err
	}
	var authority Authority
	err = s.db.WithContext(ctx).Where("workspace_id = ? AND domain = ? AND binding_key = ?", workspaceID, domain, bindingKey).First(&authority).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return Authority{WorkspaceID: workspaceID, Domain: domain, BindingKey: bindingKey, State: AuthorityStateLegacyActive}, false, nil
	}
	if err != nil {
		return Authority{}, false, fmt.Errorf("get content authority: %w", err)
	}
	return authority, true, nil
}

// ListAuthoritiesForBindings resolves only the persisted authority rows for a
// bounded set of exact bindings. Missing bindings remain implicit legacy and
// are not materialized. This prevents one authority query per product row.
func (s *Store) ListAuthoritiesForBindings(ctx context.Context, workspaceID, domain string, bindingKeys []string) (map[string]Authority, error) {
	if err := s.ensureDB(); err != nil {
		return nil, err
	}
	keys := make([]string, 0, len(bindingKeys))
	seen := make(map[string]struct{}, len(bindingKeys))
	for _, bindingKey := range bindingKeys {
		var err error
		workspaceID, domain, bindingKey, err = normalizeAuthorityBinding(workspaceID, domain, bindingKey)
		if err != nil || bindingKey == AuthorityDomainDefaultBinding {
			return nil, fmt.Errorf("content authority binding batch must be exact")
		}
		if _, exists := seen[bindingKey]; exists {
			continue
		}
		seen[bindingKey] = struct{}{}
		keys = append(keys, bindingKey)
	}
	if len(keys) > MaxAuthorityBindingBatch {
		return nil, fmt.Errorf("content authority binding batch exceeds %d", MaxAuthorityBindingBatch)
	}
	out := make(map[string]Authority, len(keys))
	if len(keys) == 0 {
		return out, nil
	}
	var rows []Authority
	if err := s.db.WithContext(ctx).
		Where("workspace_id = ? AND domain = ? AND binding_key IN ?", workspaceID, domain, keys).
		Limit(len(keys)).Find(&rows).Error; err != nil {
		return nil, fmt.Errorf("list content authorities for bindings: %w", err)
	}
	for _, row := range rows {
		out[row.BindingKey] = row
	}
	return out, nil
}

// ActivateNativeAuthority changes one exact binding from implicit or explicit
// legacy ownership to native ownership with generation CAS. It does not wait
// for, coordinate with, or reverse-sync AGS writers: the deployment contract
// accepts that a small number of writes landing in AGS during cutover may be
// lost. After this method succeeds, routing remains native and fails closed.
func (s *Store) ActivateNativeAuthority(ctx context.Context, params ActivateNativeAuthorityParams) (Authority, error) {
	if err := s.ensureDB(); err != nil {
		return Authority{}, err
	}
	if err := normalizeActivateNativeAuthorityParams(&params); err != nil {
		return Authority{}, err
	}

	var result Authority
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var current Authority
		loadErr := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("workspace_id = ? AND domain = ? AND binding_key = ?", params.WorkspaceID, params.Domain, params.BindingKey).
			First(&current).Error
		if errors.Is(loadErr, gorm.ErrRecordNotFound) {
			if params.ExpectedGeneration != 0 || params.ExpectedCheckpoint != "" {
				return ErrAuthorityConflict
			}
			id, err := newAuthorityID()
			if err != nil {
				return err
			}
			result = Authority{
				ID: id, WorkspaceID: params.WorkspaceID, Domain: params.Domain, BindingKey: params.BindingKey,
				State: AuthorityStateNativeActive, Generation: 1, Checkpoint: params.Checkpoint,
				UpdatedBy: params.Actor, CreatedAt: params.Now, UpdatedAt: params.Now,
			}
			if err := tx.Create(&result).Error; err != nil {
				return ErrAuthorityConflict
			}
			return nil
		}
		if loadErr != nil {
			return loadErr
		}
		if current.State != AuthorityStateLegacyActive || current.Generation != params.ExpectedGeneration || current.Checkpoint != params.ExpectedCheckpoint {
			return ErrAuthorityConflict
		}
		updated := tx.Model(&Authority{}).
			Where("id = ? AND state = ? AND generation = ?", current.ID, AuthorityStateLegacyActive, current.Generation).
			Updates(map[string]any{
				"state": AuthorityStateNativeActive, "generation": current.Generation + 1,
				"checkpoint": params.Checkpoint, "updated_by": params.Actor, "updated_at": params.Now,
			})
		if updated.Error != nil {
			return updated.Error
		}
		if updated.RowsAffected != 1 {
			return ErrAuthorityConflict
		}
		return tx.Where("id = ?", current.ID).First(&result).Error
	})
	if err != nil {
		if errors.Is(err, ErrAuthorityConflict) {
			return Authority{}, ErrAuthorityConflict
		}
		return Authority{}, fmt.Errorf("activate native content authority: %w", err)
	}
	return result, nil
}

func newAuthorityID() (string, error) {
	var raw [16]byte
	if _, err := rand.Read(raw[:]); err != nil {
		return "", fmt.Errorf("generate authority id: %w", err)
	}
	return "cta_" + hex.EncodeToString(raw[:]), nil
}

func normalizeAuthorityBinding(workspaceID, domain, bindingKey string) (string, string, string, error) {
	workspaceID = strings.TrimSpace(workspaceID)
	domain = strings.TrimSpace(domain)
	bindingKey = strings.TrimSpace(bindingKey)
	if workspaceID == "" || len(workspaceID) > 64 || domain == "" || len(domain) > 64 || bindingKey == "" || len(bindingKey) > 255 {
		return "", "", "", fmt.Errorf("content authority binding is invalid")
	}
	return workspaceID, domain, bindingKey, nil
}

func normalizeActivateNativeAuthorityParams(params *ActivateNativeAuthorityParams) error {
	if params == nil {
		return fmt.Errorf("content authority activation is required")
	}
	workspaceID, domain, bindingKey, err := normalizeAuthorityBinding(params.WorkspaceID, params.Domain, params.BindingKey)
	if err != nil {
		return err
	}
	params.WorkspaceID, params.Domain, params.BindingKey = workspaceID, domain, bindingKey
	if params.BindingKey == AuthorityDomainDefaultBinding {
		return fmt.Errorf("content authority activation requires an exact binding")
	}
	params.Checkpoint = strings.TrimSpace(params.Checkpoint)
	params.ExpectedCheckpoint = strings.TrimSpace(params.ExpectedCheckpoint)
	params.Actor = strings.TrimSpace(params.Actor)
	if params.Checkpoint == "" || len(params.Checkpoint) > 255 || len(params.ExpectedCheckpoint) > 255 || params.Actor == "" || len(params.Actor) > 64 || params.Now.IsZero() {
		return fmt.Errorf("content authority activation identity is invalid")
	}
	params.Now = params.Now.UTC()
	return nil
}

func normalizeLegacyAuthorityFenceParams(params *AcquireLegacyAuthorityFenceParams) (string, error) {
	if params == nil {
		return "", fmt.Errorf("legacy content authority fence is required")
	}
	workspaceID, domain, bindingKey, err := normalizeAuthorityBinding(params.WorkspaceID, params.Domain, params.BindingKey)
	if err != nil || bindingKey == AuthorityDomainDefaultBinding {
		return "", fmt.Errorf("legacy content authority fence requires an exact binding")
	}
	params.WorkspaceID, params.Domain, params.BindingKey = workspaceID, domain, bindingKey
	params.Token = strings.TrimSpace(params.Token)
	params.Actor = strings.TrimSpace(params.Actor)
	params.Now = params.Now.UTC()
	params.ExpiresAt = params.ExpiresAt.UTC()
	if params.Token == "" || len(params.Token) > 64 || strings.Contains(params.Token, "|") || params.Actor == "" || len(params.Actor) > 64 || params.Now.IsZero() || !params.ExpiresAt.After(params.Now) {
		return "", fmt.Errorf("legacy content authority fence identity is invalid")
	}
	checkpoint := legacyAuthorityFencePrefix + params.Token + "|" + strconv.FormatInt(params.ExpiresAt.UnixMilli(), 10)
	if len(checkpoint) > 255 {
		return "", fmt.Errorf("legacy content authority fence is too large")
	}
	return checkpoint, nil
}

func decodeLegacyAuthorityFence(checkpoint string) (string, time.Time, error) {
	if !strings.HasPrefix(checkpoint, legacyAuthorityFencePrefix) {
		return "", time.Time{}, fmt.Errorf("legacy content authority fence is invalid")
	}
	parts := strings.Split(strings.TrimPrefix(checkpoint, legacyAuthorityFencePrefix), "|")
	if len(parts) != 2 || parts[0] == "" || len(parts[0]) > 64 {
		return "", time.Time{}, fmt.Errorf("legacy content authority fence is invalid")
	}
	millis, err := strconv.ParseInt(parts[1], 10, 64)
	if err != nil || millis <= 0 {
		return "", time.Time{}, fmt.Errorf("legacy content authority fence is invalid")
	}
	return parts[0], time.UnixMilli(millis).UTC(), nil
}
