package automation

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

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

const (
	defaultAutomationRepoStatusMaxAttempts = 10
	maxAutomationRepoStatusContextLength   = 255
	maxAutomationRepoStatusDescription     = 255
	maxAutomationRepoStatusLastError       = 4096
)

// AutomationRepoStatusAttemptResult reports whether a leased outbox row accepted
// a delivery failure update and whether retries are exhausted.
type AutomationRepoStatusAttemptResult struct {
	Updated bool
	Failed  bool
}

func enqueueAutomationRepoStatusTerminalTx(tx *gorm.DB, run AutomationRun, now time.Time) error {
	state := automationRepoStatusStateForRunStatus(run.Status)
	if state == "" {
		return nil
	}
	return upsertAutomationRepoStatusTx(tx, run, state, now)
}

func upsertAutomationRepoStatusTx(tx *gorm.DB, run AutomationRun, state string, now time.Time) error {
	row, ok := automationRepoStatusOutboxForRun(run, state, now)
	if !ok {
		return nil
	}
	updated, err := updateAutomationRepoStatusTx(tx, row)
	if err != nil {
		return err
	}
	if updated {
		return nil
	}
	result := tx.Clauses(clause.OnConflict{
		Columns: []clause.Column{
			{Name: "workspace_id"},
			{Name: "repository_owner"},
			{Name: "repository_name"},
			{Name: "commit_sha"},
			{Name: "context"},
		},
		DoNothing: true,
	}).Create(&row)
	if result.Error != nil {
		return fmt.Errorf("insert automation repo status outbox: %w", result.Error)
	}
	if result.RowsAffected > 0 {
		return nil
	}
	_, err = updateAutomationRepoStatusTx(tx, row)
	return err
}

func updateAutomationRepoStatusTx(tx *gorm.DB, row AutomationRepoStatusOutbox) (bool, error) {
	activeLease := "automation_repo_status_outbox.lease_expires_at IS NOT NULL AND automation_repo_status_outbox.lease_expires_at > ?"
	result := tx.Model(&AutomationRepoStatusOutbox{}).
		Where("workspace_id = ? AND repository_owner = ? AND repository_name = ? AND commit_sha = ? AND context = ?", row.WorkspaceID, row.RepositoryOwner, row.RepositoryName, row.CommitSHA, row.Context).
		Where("automation_repo_status_outbox.run_id = ? OR automation_repo_status_outbox.run_created_at < ?", row.RunID, row.RunCreatedAt).
		Updates(map[string]any{
			"run_id":           row.RunID,
			"run_created_at":   row.RunCreatedAt,
			"repository_owner": row.RepositoryOwner,
			"repository_name":  row.RepositoryName,
			"commit_sha":       row.CommitSHA,
			"state":            row.State,
			"description":      row.Description,
			"sequence":         gorm.Expr("automation_repo_status_outbox.sequence + 1"),
			"status":           AutomationRepoStatusOutboxStatusPending,
			"attempts":         0,
			"max_attempts":     row.MaxAttempts,
			"next_attempt_at":  row.NextAttemptAt,
			"lease_owner":      gorm.Expr("CASE WHEN "+activeLease+" THEN automation_repo_status_outbox.lease_owner ELSE '' END", row.UpdatedAt),
			"lease_expires_at": gorm.Expr("CASE WHEN "+activeLease+" THEN automation_repo_status_outbox.lease_expires_at ELSE NULL END", row.UpdatedAt),
			"last_error":       "",
			"delivered_at":     nil,
			"updated_at":       row.UpdatedAt,
		})
	if result.Error != nil {
		return false, fmt.Errorf("update automation repo status outbox: %w", result.Error)
	}
	return result.RowsAffected > 0, nil
}

func automationRepoStatusOutboxForRun(run AutomationRun, state string, now time.Time) (AutomationRepoStatusOutbox, bool) {
	run.WorkspaceID = strings.TrimSpace(run.WorkspaceID)
	run.ID = strings.TrimSpace(run.ID)
	run.RepositoryOwner = strings.TrimSpace(run.RepositoryOwner)
	run.RepositoryName = strings.TrimSpace(run.RepositoryName)
	run.CommitSHA = strings.TrimSpace(run.CommitSHA)
	context := automationRepoStatusContext(run.DefinitionPath)
	if run.WorkspaceID == "" || run.ID == "" || run.RepositoryOwner == "" || run.RepositoryName == "" || run.CommitSHA == "" || context == "" {
		return AutomationRepoStatusOutbox{}, false
	}
	if now.IsZero() {
		now = time.Now().UTC()
	}
	runCreatedAt := run.CreatedAt
	if runCreatedAt.IsZero() {
		runCreatedAt = now
	}
	state = strings.TrimSpace(state)
	if state == "" {
		return AutomationRepoStatusOutbox{}, false
	}
	return AutomationRepoStatusOutbox{
		ID:              automationRepoStatusOutboxID(run.WorkspaceID, run.RepositoryOwner, run.RepositoryName, run.CommitSHA, context),
		WorkspaceID:     run.WorkspaceID,
		RunID:           run.ID,
		RunCreatedAt:    runCreatedAt,
		RepositoryOwner: run.RepositoryOwner,
		RepositoryName:  run.RepositoryName,
		CommitSHA:       run.CommitSHA,
		Context:         context,
		State:           state,
		Description:     truncateAutomationRepoStatusString(automationRepoStatusDescription(run, state), maxAutomationRepoStatusDescription),
		Sequence:        1,
		Status:          AutomationRepoStatusOutboxStatusPending,
		MaxAttempts:     defaultAutomationRepoStatusMaxAttempts,
		NextAttemptAt:   now,
		CreatedAt:       now,
		UpdatedAt:       now,
	}, true
}

func automationRepoStatusContext(definitionPath string) string {
	path := strings.TrimSpace(definitionPath)
	if path == "" {
		return ""
	}
	raw := "meshx/automation/" + path
	if len(raw) <= maxAutomationRepoStatusContextLength {
		return raw
	}
	sum := sha256.Sum256([]byte(raw))
	suffix := ":" + hex.EncodeToString(sum[:8])
	keep := maxAutomationRepoStatusContextLength - len(suffix)
	if keep < len("meshx/automation/") {
		keep = len("meshx/automation/")
	}
	return truncateAutomationRepoStatusString(raw, keep) + suffix
}

func automationRepoStatusOutboxID(workspaceID string, repositoryOwner string, repositoryName string, commitSHA string, context string) string {
	sum := sha256.Sum256([]byte(strings.TrimSpace(workspaceID) + "\x1f" + strings.TrimSpace(repositoryOwner) + "\x1f" + strings.TrimSpace(repositoryName) + "\x1f" + strings.TrimSpace(commitSHA) + "\x1f" + strings.TrimSpace(context)))
	return "wfrs_" + hex.EncodeToString(sum[:16])
}

func automationRepoStatusStateForRunStatus(status string) string {
	switch strings.TrimSpace(status) {
	case AutomationRunStatusSucceeded:
		return AutomationRepoStatusStateSuccess
	case AutomationRunStatusFailed:
		return AutomationRepoStatusStateFailure
	case AutomationRunStatusCanceled:
		return AutomationRepoStatusStateError
	default:
		return ""
	}
}

func automationRepoStatusDescription(run AutomationRun, state string) string {
	name := strings.TrimSpace(run.Name)
	if name == "" {
		name = strings.TrimSpace(run.RunName)
	}
	if name == "" {
		name = strings.TrimSpace(run.DefinitionPath)
	}
	if name == "" {
		name = "run"
	}
	switch state {
	case AutomationRepoStatusStatePending:
		return "Automation " + name + " is running"
	case AutomationRepoStatusStateSuccess:
		return "Automation " + name + " succeeded"
	case AutomationRepoStatusStateFailure:
		return "Automation " + name + " failed"
	case AutomationRepoStatusStateError:
		return "Automation " + name + " was canceled"
	default:
		return "Automation " + name + " status changed"
	}
}

func truncateAutomationRepoStatusString(value string, limit int) string {
	value = strings.TrimSpace(value)
	if limit <= 0 || len(value) <= limit {
		return value
	}
	end := 0
	for end < len(value) {
		_, size := utf8.DecodeRuneInString(value[end:])
		if end+size > limit {
			break
		}
		end += size
	}
	return value[:end]
}

// LeaseDueAutomationRepoStatuses claims due repo-status outbox rows for delivery.
func (s *Store) LeaseDueAutomationRepoStatuses(ctx context.Context, now time.Time, leaseOwner string, leaseUntil time.Time, limit int, workspaceScope ...[]string) ([]AutomationRepoStatusOutbox, error) {
	if err := s.ensureDB(); err != nil {
		return nil, err
	}
	leaseOwner = strings.TrimSpace(leaseOwner)
	if leaseOwner == "" {
		return nil, fmt.Errorf("automation repo status lease owner is required")
	}
	if limit <= 0 {
		limit = 25
	}
	if now.IsZero() {
		now = time.Now().UTC()
	}
	if leaseUntil.IsZero() || !leaseUntil.After(now) {
		leaseUntil = now.Add(time.Minute)
	}
	var leased []AutomationRepoStatusOutbox
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var candidates []AutomationRepoStatusOutbox
		query := tx.Select("id", "workspace_id", "run_id", "repository_owner", "repository_name", "commit_sha", "context", "state", "description", "sequence", "attempts", "max_attempts").
			Where("status = ? AND next_attempt_at <= ? AND (lease_expires_at IS NULL OR lease_expires_at <= ?)", AutomationRepoStatusOutboxStatusPending, now, now)
		query = applyWorkspaceScope(query, "workspace_id", workspaceScope)
		if err := query.
			Order("next_attempt_at ASC").
			Order("id ASC").
			Limit(limit).
			Find(&candidates).Error; err != nil {
			return err
		}
		for _, candidate := range candidates {
			result := tx.Model(&AutomationRepoStatusOutbox{}).
				Where("id = ? AND sequence = ? AND status = ? AND next_attempt_at <= ? AND (lease_expires_at IS NULL OR lease_expires_at <= ?)", candidate.ID, candidate.Sequence, AutomationRepoStatusOutboxStatusPending, now, now).
				Updates(map[string]any{
					"lease_owner":      leaseOwner,
					"lease_expires_at": leaseUntil,
					"updated_at":       now,
				})
			if result.Error != nil {
				return result.Error
			}
			if result.RowsAffected == 0 {
				continue
			}
			candidate.LeaseOwner = leaseOwner
			candidate.LeaseExpiresAt = &leaseUntil
			candidate.UpdatedAt = now
			leased = append(leased, candidate)
		}
		return nil
	})
	if err != nil {
		return nil, fmt.Errorf("lease automation repo status outbox: %w", err)
	}
	return leased, nil
}

// MarkAutomationRepoStatusDelivered marks a leased row delivered only if the
// desired sequence has not been superseded by a newer run state.
func (s *Store) MarkAutomationRepoStatusDelivered(ctx context.Context, row AutomationRepoStatusOutbox, leaseOwner string, now time.Time) (bool, error) {
	if err := s.ensureDB(); err != nil {
		return false, err
	}
	result := s.db.WithContext(ctx).Model(&AutomationRepoStatusOutbox{}).
		Where("id = ? AND lease_owner = ? AND sequence = ? AND status = ?", strings.TrimSpace(row.ID), strings.TrimSpace(leaseOwner), row.Sequence, AutomationRepoStatusOutboxStatusPending).
		Updates(map[string]any{
			"status":           AutomationRepoStatusOutboxStatusDelivered,
			"lease_owner":      "",
			"lease_expires_at": nil,
			"last_error":       "",
			"delivered_at":     now,
			"updated_at":       now,
		})
	if result.Error != nil {
		return false, fmt.Errorf("mark automation repo status delivered: %w", result.Error)
	}
	if result.RowsAffected == 0 {
		if err := s.releaseSupersededAutomationRepoStatusLease(ctx, row, leaseOwner, now); err != nil {
			return false, err
		}
	}
	return result.RowsAffected > 0, nil
}

// MarkAutomationRepoStatusAttemptFailed records a delivery failure while keeping
// stale leases from overwriting a newer desired sequence.
func (s *Store) MarkAutomationRepoStatusAttemptFailed(ctx context.Context, row AutomationRepoStatusOutbox, leaseOwner string, err error, now time.Time) (AutomationRepoStatusAttemptResult, error) {
	if err := s.ensureDB(); err != nil {
		return AutomationRepoStatusAttemptResult{}, err
	}
	attempts := row.Attempts + 1
	maxAttempts := row.MaxAttempts
	if maxAttempts <= 0 {
		maxAttempts = defaultAutomationRepoStatusMaxAttempts
	}
	status := AutomationRepoStatusOutboxStatusPending
	nextAttemptAt := now.Add(RetryBackoff(attempts))
	failed := false
	if attempts >= maxAttempts {
		status = AutomationRepoStatusOutboxStatusFailed
		nextAttemptAt = now
		failed = true
	}
	lastError := ""
	if err != nil {
		lastError = truncateAutomationRepoStatusString(err.Error(), maxAutomationRepoStatusLastError)
	}
	result := s.db.WithContext(ctx).Model(&AutomationRepoStatusOutbox{}).
		Where("id = ? AND lease_owner = ? AND sequence = ? AND status = ?", strings.TrimSpace(row.ID), strings.TrimSpace(leaseOwner), row.Sequence, AutomationRepoStatusOutboxStatusPending).
		Updates(map[string]any{
			"status":           status,
			"attempts":         attempts,
			"next_attempt_at":  nextAttemptAt,
			"lease_owner":      "",
			"lease_expires_at": nil,
			"last_error":       lastError,
			"updated_at":       now,
		})
	if result.Error != nil {
		return AutomationRepoStatusAttemptResult{}, fmt.Errorf("mark automation repo status attempt failed: %w", result.Error)
	}
	if result.RowsAffected == 0 {
		if err := s.releaseSupersededAutomationRepoStatusLease(ctx, row, leaseOwner, now); err != nil {
			return AutomationRepoStatusAttemptResult{}, err
		}
		return AutomationRepoStatusAttemptResult{}, nil
	}
	return AutomationRepoStatusAttemptResult{Updated: true, Failed: failed}, nil
}

func (s *Store) releaseSupersededAutomationRepoStatusLease(ctx context.Context, row AutomationRepoStatusOutbox, leaseOwner string, now time.Time) error {
	if row.LeaseExpiresAt == nil {
		return nil
	}
	result := s.db.WithContext(ctx).Model(&AutomationRepoStatusOutbox{}).
		Where("id = ? AND lease_owner = ? AND sequence > ? AND status = ? AND lease_expires_at IS NOT NULL AND lease_expires_at <= ?", strings.TrimSpace(row.ID), strings.TrimSpace(leaseOwner), row.Sequence, AutomationRepoStatusOutboxStatusPending, *row.LeaseExpiresAt).
		Updates(map[string]any{
			"lease_owner":      "",
			"lease_expires_at": nil,
			"updated_at":       now,
		})
	if result.Error != nil {
		return fmt.Errorf("release superseded automation repo status lease: %w", result.Error)
	}
	return nil
}
