package migration

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

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

// Store owns durable exact-target migration jobs and verified legacy mappings.
type Store struct{ db *gorm.DB }

func New(db *gorm.DB) *Store { return &Store{db: db} }

func (s *Store) ensureDB() error {
	if s == nil || s.db == nil {
		return fmt.Errorf("content migration store is not configured")
	}
	return nil
}

const (
	MigrationCursorPending   = "pending"
	MigrationCursorPublished = "published"
	MigrationCursorMapped    = "mapped"
	MigrationCursorCompleted = "completed"

	MaxMigrationAttempts      = 10
	MaxMigrationLeaseDuration = 5 * time.Minute
	MaxMigrationErrorBytes    = 2048
)

var (
	ErrMigrationConflict  = errors.New("content migration conflict")
	ErrMigrationLeaseHeld = errors.New("content migration lease held")
)

type AcquireParams struct {
	ID                    string
	WorkspaceID           string
	Domain                string
	BindingKey            string
	SourceCursor          string
	RepositoryID          string
	CommitSHA             string
	ExpectedHeadVersionID string
	LeaseOwner            string
	LeaseUntil            time.Time
	Now                   time.Time
}

// AcquireExactJob creates or leases one exact migration target.
// It never enumerates pending jobs or unrelated workspace bindings.
func (s *Store) AcquireExactJob(ctx context.Context, params AcquireParams) (Job, error) {
	if err := s.ensureDB(); err != nil {
		return Job{}, err
	}
	params.ID = strings.TrimSpace(params.ID)
	params.WorkspaceID = strings.TrimSpace(params.WorkspaceID)
	params.Domain = strings.TrimSpace(params.Domain)
	params.BindingKey = strings.TrimSpace(params.BindingKey)
	params.SourceCursor = strings.TrimSpace(params.SourceCursor)
	params.RepositoryID = strings.TrimSpace(params.RepositoryID)
	params.CommitSHA = strings.TrimSpace(params.CommitSHA)
	params.ExpectedHeadVersionID = strings.TrimSpace(params.ExpectedHeadVersionID)
	params.LeaseOwner = strings.TrimSpace(params.LeaseOwner)
	params.Now = params.Now.UTC()
	params.LeaseUntil = params.LeaseUntil.UTC()
	if params.ID == "" || params.WorkspaceID == "" || params.Domain == "" || params.BindingKey == "" || params.SourceCursor == "" ||
		params.RepositoryID == "" || params.CommitSHA == "" || params.LeaseOwner == "" || params.Now.IsZero() ||
		!params.LeaseUntil.After(params.Now) || params.LeaseUntil.Sub(params.Now) > MaxMigrationLeaseDuration {
		return Job{}, fmt.Errorf("content migration exact target and bounded lease are required")
	}
	var leased Job
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var job Job
		err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("workspace_id = ? AND domain = ? AND binding_key = ? AND source_cursor = ?", params.WorkspaceID, params.Domain, params.BindingKey, params.SourceCursor).
			First(&job).Error
		if errors.Is(err, gorm.ErrRecordNotFound) {
			job = Job{
				ID: params.ID, WorkspaceID: params.WorkspaceID, Domain: params.Domain, BindingKey: params.BindingKey,
				SourceCursor: params.SourceCursor, RepositoryID: params.RepositoryID, CommitSHA: params.CommitSHA,
				ExpectedHeadVersionID: params.ExpectedHeadVersionID, Cursor: MigrationCursorPending,
				CreatedAt: params.Now, UpdatedAt: params.Now,
			}
			if err := tx.Create(&job).Error; err != nil {
				return fmt.Errorf("create exact content migration job: %w", err)
			}
		} else if err != nil {
			return fmt.Errorf("load exact content migration job: %w", err)
		}
		if job.RepositoryID != params.RepositoryID || job.CommitSHA != params.CommitSHA || job.ExpectedHeadVersionID != params.ExpectedHeadVersionID {
			return ErrMigrationConflict
		}
		if job.Cursor == MigrationCursorCompleted {
			leased = job
			return nil
		}
		if job.LeaseUntil != nil && job.LeaseUntil.After(params.Now) {
			return ErrMigrationLeaseHeld
		}
		if job.Attempts >= MaxMigrationAttempts {
			return fmt.Errorf("%w: attempt limit reached", ErrMigrationConflict)
		}
		job.Attempts++
		job.LeaseOwner = params.LeaseOwner
		job.LeaseUntil = &params.LeaseUntil
		job.LastError = ""
		job.UpdatedAt = params.Now
		if err := tx.Model(&Job{}).Where("id = ?", job.ID).Updates(map[string]any{
			"attempts": job.Attempts, "lease_owner": job.LeaseOwner, "lease_until": job.LeaseUntil,
			"last_error": "", "updated_at": job.UpdatedAt,
		}).Error; err != nil {
			return fmt.Errorf("lease exact content migration job: %w", err)
		}
		leased = job
		return nil
	})
	return leased, err
}

type CheckpointPublishedParams struct {
	JobID            string
	LeaseOwner       string
	ContentID        string
	ContentVersionID string
	RootDigest       string
	FileCount        int
	SizeBytes        int64
	Now              time.Time
}

func (s *Store) CheckpointPublished(ctx context.Context, params CheckpointPublishedParams) (Job, error) {
	if err := s.ensureDB(); err != nil {
		return Job{}, err
	}
	params.JobID = strings.TrimSpace(params.JobID)
	params.LeaseOwner = strings.TrimSpace(params.LeaseOwner)
	params.ContentID = strings.TrimSpace(params.ContentID)
	params.ContentVersionID = strings.TrimSpace(params.ContentVersionID)
	params.RootDigest = strings.TrimSpace(params.RootDigest)
	params.Now = params.Now.UTC()
	if params.JobID == "" || params.LeaseOwner == "" || params.ContentID == "" || params.ContentVersionID == "" || params.RootDigest == "" || params.FileCount <= 0 || params.SizeBytes < 0 || params.Now.IsZero() {
		return Job{}, fmt.Errorf("content migration published checkpoint is invalid")
	}
	var published Job
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var job Job
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", params.JobID).First(&job).Error; err != nil {
			return err
		}
		if job.LeaseOwner != params.LeaseOwner || job.LeaseUntil == nil || !job.LeaseUntil.After(params.Now) {
			return ErrMigrationLeaseHeld
		}
		if job.Cursor == MigrationCursorPublished {
			if job.ContentID != params.ContentID || job.ContentVersionID != params.ContentVersionID || job.RootDigest != params.RootDigest || job.FileCount != params.FileCount || job.SizeBytes != params.SizeBytes {
				return ErrMigrationConflict
			}
			published = job
			return nil
		}
		if job.Cursor != MigrationCursorPending {
			return ErrMigrationConflict
		}
		if err := tx.Model(&Job{}).Where("id = ?", job.ID).Updates(map[string]any{
			"content_id": params.ContentID, "content_version_id": params.ContentVersionID, "root_digest": params.RootDigest,
			"file_count": params.FileCount, "size_bytes": params.SizeBytes, "cursor": MigrationCursorPublished, "updated_at": params.Now,
		}).Error; err != nil {
			return fmt.Errorf("checkpoint content migration publication: %w", err)
		}
		job.ContentID = params.ContentID
		job.ContentVersionID = params.ContentVersionID
		job.RootDigest = params.RootDigest
		job.FileCount = params.FileCount
		job.SizeBytes = params.SizeBytes
		job.Cursor = MigrationCursorPublished
		job.UpdatedAt = params.Now
		published = job
		return nil
	})
	return published, err
}

type VerifyMappingParams struct {
	MapID      string
	JobID      string
	LeaseOwner string
	Now        time.Time
}

// VerifyMapping atomically records the exact legacy source map and
// advances the job cursor. Replays must match every immutable fact.
func (s *Store) VerifyMapping(ctx context.Context, params VerifyMappingParams) (Job, error) {
	if err := s.ensureDB(); err != nil {
		return Job{}, err
	}
	params.MapID = strings.TrimSpace(params.MapID)
	params.JobID = strings.TrimSpace(params.JobID)
	params.LeaseOwner = strings.TrimSpace(params.LeaseOwner)
	params.Now = params.Now.UTC()
	if params.MapID == "" || params.JobID == "" || params.LeaseOwner == "" || params.Now.IsZero() {
		return Job{}, fmt.Errorf("content migration mapping identity is invalid")
	}
	var mapped Job
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var job Job
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", params.JobID).First(&job).Error; err != nil {
			return err
		}
		if job.LeaseOwner != params.LeaseOwner || job.LeaseUntil == nil || !job.LeaseUntil.After(params.Now) {
			return ErrMigrationLeaseHeld
		}
		if job.Cursor != MigrationCursorPublished && job.Cursor != MigrationCursorMapped {
			return ErrMigrationConflict
		}
		row := Mapping{
			ID: params.MapID, WorkspaceID: job.WorkspaceID, RepositoryID: job.RepositoryID, CommitSHA: job.CommitSHA,
			ContentID: job.ContentID, ContentVersionID: job.ContentVersionID, Model: "tree",
			RootDigest: job.RootDigest, FileCount: job.FileCount, SizeBytes: job.SizeBytes,
			VerifiedAt: &params.Now, CreatedAt: params.Now, UpdatedAt: params.Now,
		}
		var existing Mapping
		err := tx.Where("ags_repository_id = ? AND ags_commit_sha = ?", row.RepositoryID, row.CommitSHA).First(&existing).Error
		switch {
		case err == nil:
			if existing.WorkspaceID != row.WorkspaceID || existing.ContentID != row.ContentID || existing.ContentVersionID != row.ContentVersionID ||
				existing.Model != row.Model || existing.RootDigest != row.RootDigest || existing.FileCount != row.FileCount || existing.SizeBytes != row.SizeBytes || existing.VerifiedAt == nil {
				return ErrMigrationConflict
			}
		case errors.Is(err, gorm.ErrRecordNotFound):
			if err := tx.Create(&row).Error; err != nil {
				return fmt.Errorf("create verified content migration map: %w", err)
			}
		default:
			return err
		}
		if err := tx.Model(&Job{}).Where("id = ?", job.ID).Updates(map[string]any{
			"cursor": MigrationCursorMapped, "updated_at": params.Now,
		}).Error; err != nil {
			return fmt.Errorf("checkpoint verified content migration map: %w", err)
		}
		job.Cursor = MigrationCursorMapped
		job.UpdatedAt = params.Now
		mapped = job
		return nil
	})
	return mapped, err
}

func (s *Store) CompleteJob(ctx context.Context, jobID, leaseOwner string, now time.Time) (Job, error) {
	if err := s.ensureDB(); err != nil {
		return Job{}, err
	}
	jobID = strings.TrimSpace(jobID)
	leaseOwner = strings.TrimSpace(leaseOwner)
	now = now.UTC()
	if jobID == "" || leaseOwner == "" || now.IsZero() {
		return Job{}, fmt.Errorf("content migration completion identity is invalid")
	}
	result := s.db.WithContext(ctx).Model(&Job{}).
		Where("id = ? AND lease_owner = ? AND lease_until > ? AND cursor = ?", jobID, leaseOwner, now, MigrationCursorMapped).
		Updates(map[string]any{
			"cursor": MigrationCursorCompleted, "lease_owner": "", "lease_until": nil, "last_error": "", "completed_at": now, "updated_at": now,
		})
	if result.Error != nil {
		return Job{}, fmt.Errorf("complete content migration job: %w", result.Error)
	}
	if result.RowsAffected != 1 {
		return Job{}, ErrMigrationLeaseHeld
	}
	return s.getJobByID(ctx, jobID)
}

func (s *Store) ReleaseFailure(ctx context.Context, jobID, leaseOwner string, cause error, now time.Time) error {
	if err := s.ensureDB(); err != nil {
		return err
	}
	jobID = strings.TrimSpace(jobID)
	leaseOwner = strings.TrimSpace(leaseOwner)
	now = now.UTC()
	if jobID == "" || leaseOwner == "" || now.IsZero() {
		return fmt.Errorf("content migration failure identity is invalid")
	}
	message := ""
	if cause != nil {
		message = cause.Error()
	}
	if len(message) > MaxMigrationErrorBytes {
		message = message[:MaxMigrationErrorBytes]
	}
	result := s.db.WithContext(ctx).Model(&Job{}).
		Where("id = ? AND lease_owner = ?", jobID, leaseOwner).
		Updates(map[string]any{"lease_owner": "", "lease_until": nil, "last_error": message, "updated_at": now})
	if result.Error != nil {
		return fmt.Errorf("release failed content migration job: %w", result.Error)
	}
	if result.RowsAffected != 1 {
		return ErrMigrationLeaseHeld
	}
	return nil
}

func (s *Store) GetJob(ctx context.Context, workspaceID, domain, bindingKey, sourceCursor string) (Job, error) {
	if err := s.ensureDB(); err != nil {
		return Job{}, err
	}
	var job Job
	err := s.db.WithContext(ctx).
		Where("workspace_id = ? AND domain = ? AND binding_key = ? AND source_cursor = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(domain), strings.TrimSpace(bindingKey), strings.TrimSpace(sourceCursor)).
		First(&job).Error
	return job, err
}

func (s *Store) GetMapping(ctx context.Context, repositoryID, commitSHA string) (Mapping, error) {
	if err := s.ensureDB(); err != nil {
		return Mapping{}, err
	}
	var row Mapping
	err := s.db.WithContext(ctx).
		Where("ags_repository_id = ? AND ags_commit_sha = ?", strings.TrimSpace(repositoryID), strings.TrimSpace(commitSHA)).
		First(&row).Error
	return row, err
}

func (s *Store) getJobByID(ctx context.Context, id string) (Job, error) {
	if err := s.ensureDB(); err != nil {
		return Job{}, err
	}
	var job Job
	err := s.db.WithContext(ctx).Where("id = ?", strings.TrimSpace(id)).First(&job).Error
	return job, err
}
