// Package asynctask 提供业务自有任务表复用的 PostgreSQL 条件更新 Adapter。
package asynctask

import (
	"context"
	stderrors "errors"
	"regexp"
	"time"

	"gorm.io/gorm"

	contract "github.com/break/junhong_cmp_fiber/pkg/asynctask"
)

var identifierPattern = regexp.MustCompile(`^[a-z][a-z0-9_]*$`)

// Definition 描述业务自有任务表如何映射统一字段。
type Definition struct {
	Table              string
	IDColumn           string
	StatusColumn       string
	LeaseOwnerColumn   string
	LeaseExpiresColumn string
	TotalColumn        string
	SuccessColumn      string
	FailedColumn       string
	ProgressColumn     string
	ErrorCodeColumn    string
	ErrorSummaryColumn string
	StartedAtColumn    string
	CompletedAtColumn  string
	UpdatedAtColumn    string
}

// Store 对一个已注册的业务任务表执行统一条件更新。
type Store struct {
	db         *gorm.DB
	definition Definition
}

// NewStore 创建业务任务表 Adapter，不创建或迁移任何万能任务表。
func NewStore(db *gorm.DB, definition Definition) (*Store, error) {
	columns := []string{
		definition.Table, definition.IDColumn, definition.StatusColumn, definition.LeaseOwnerColumn,
		definition.LeaseExpiresColumn, definition.TotalColumn, definition.SuccessColumn,
		definition.FailedColumn, definition.ProgressColumn, definition.ErrorCodeColumn,
		definition.ErrorSummaryColumn, definition.StartedAtColumn, definition.CompletedAtColumn, definition.UpdatedAtColumn,
	}
	for _, identifier := range columns {
		if !identifierPattern.MatchString(identifier) {
			return nil, stderrors.New("异步任务表或字段标识不合法")
		}
	}
	return &Store{db: db, definition: definition}, nil
}

// Claim 使用预期状态和过期租约条件领取任务。
func (s *Store) Claim(ctx context.Context, id any, owner string, now time.Time, duration time.Duration) (bool, error) {
	if owner == "" || duration <= 0 {
		return false, stderrors.New("任务租约所有者和时长不能为空")
	}
	d := s.definition
	result := s.db.WithContext(ctx).Table(d.Table).
		Where(d.IDColumn+" = ? AND ("+d.StatusColumn+" = ? OR ("+d.StatusColumn+" = ? AND "+d.LeaseExpiresColumn+" <= ?))",
			id, contract.StatusPending, contract.StatusProcessing, now).
		Updates(map[string]any{
			d.StatusColumn: contract.StatusProcessing, d.LeaseOwnerColumn: owner,
			d.LeaseExpiresColumn: now.Add(duration), d.StartedAtColumn: gorm.Expr("COALESCE("+d.StartedAtColumn+", ?)", now),
			d.UpdatedAtColumn: now,
		})
	return result.RowsAffected == 1, result.Error
}

// Renew 只允许当前所有者在租约有效时续期处理中任务。
func (s *Store) Renew(ctx context.Context, id any, owner string, now time.Time, duration time.Duration) (bool, error) {
	if owner == "" || duration <= 0 {
		return false, stderrors.New("任务租约所有者和时长不能为空")
	}
	d := s.definition
	updated := s.db.WithContext(ctx).Table(d.Table).
		Where(d.IDColumn+" = ? AND "+d.StatusColumn+" = ? AND "+d.LeaseOwnerColumn+" = ? AND "+d.LeaseExpiresColumn+" > ?",
			id, contract.StatusProcessing, owner, now).
		Updates(map[string]any{d.LeaseExpiresColumn: now.Add(duration), d.UpdatedAtColumn: now})
	return updated.RowsAffected == 1, updated.Error
}

// Finish 只允许有效租约所有者把处理中任务推进到统一终态。
func (s *Store) Finish(ctx context.Context, id any, owner string, result contract.TerminalResult, now time.Time) (bool, error) {
	projection, err := contract.NewTerminalProjection(result)
	if err != nil {
		return false, err
	}
	d := s.definition
	updated := s.db.WithContext(ctx).Table(d.Table).
		Where(d.IDColumn+" = ? AND "+d.StatusColumn+" = ? AND "+d.LeaseOwnerColumn+" = ? AND "+d.LeaseExpiresColumn+" > ?",
			id, contract.StatusProcessing, owner, now).
		Updates(map[string]any{
			d.StatusColumn: projection.Status, d.TotalColumn: projection.TotalCount,
			d.SuccessColumn: projection.SuccessCount, d.FailedColumn: projection.FailedCount,
			d.ProgressColumn: projection.Progress, d.ErrorCodeColumn: projection.ErrorCode,
			d.ErrorSummaryColumn: projection.ErrorSummary, d.CompletedAtColumn: now,
			d.LeaseOwnerColumn: nil, d.LeaseExpiresColumn: nil, d.UpdatedAtColumn: now,
		})
	return updated.RowsAffected == 1, updated.Error
}

// Cancel 只允许业务明确支持时从待处理或处理中进入已取消。
func (s *Store) Cancel(ctx context.Context, id any, now time.Time) (bool, error) {
	d := s.definition
	updated := s.db.WithContext(ctx).Table(d.Table).
		Where(d.IDColumn+" = ? AND "+d.StatusColumn+" IN ?", id, []int{contract.StatusPending, contract.StatusProcessing}).
		Updates(map[string]any{
			d.StatusColumn: contract.StatusCancelled, d.ProgressColumn: 100,
			d.CompletedAtColumn: now, d.LeaseOwnerColumn: nil, d.LeaseExpiresColumn: nil, d.UpdatedAtColumn: now,
		})
	return updated.RowsAffected == 1, updated.Error
}
