package service

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/pingcap/agent-git-service/internal/db"

	"gorm.io/gorm"
)

// GetBranchProtection returns the branch protection rules for a specific branch.
func (s *Service) GetBranchProtection(ctx context.Context, repoID uint, branch string) (*db.BranchProtection, error) {
	var bp db.BranchProtection
	err := s.DBForCtx(ctx).
		Where("repository_id = ? AND branch_name = ?", repoID, branch).
		First(&bp).Error
	if err != nil {
		return nil, wrapErr(err)
	}
	if err := s.migrateLegacyRequiredStatusCheckProviders(ctx, &bp); err != nil {
		return nil, err
	}
	return &bp, nil
}

// UpdateBranchProtection creates or updates branch protection rules.
func (s *Service) UpdateBranchProtection(ctx context.Context, bp *db.BranchProtection) error {
	// Upsert based on unique index (repository_id, branch_name)
	var existing db.BranchProtection
	err := s.DBForCtx(ctx).
		Where("repository_id = ? AND branch_name = ?", bp.RepositoryID, bp.BranchName).
		First(&existing).Error

	if err == nil {
		// A fresh PUT has no internal binding and must re-run GitHub's
		// write-time provider selection even when the public JSON is unchanged.
		// Subresource updates load and carry the existing binding, so unrelated
		// mutations preserve it unless they also change the checks JSON.
		resolvedChecks := bp.ResolvedRequiredStatusChecksJSON
		bindingPending := bp.RequiredStatusChecksProviderBindingPending
		if bp.RequiredStatusChecksJSON != existing.RequiredStatusChecksJSON || strings.TrimSpace(resolvedChecks) == "" {
			var resolveErr error
			resolvedChecks, resolveErr = s.resolveRequiredStatusCheckProviders(ctx, bp.RepositoryID, bp.RequiredStatusChecksJSON)
			if resolveErr != nil {
				return resolveErr
			}
			bindingPending = false
		}
		// Update mutable fields only. Save() would overwrite created_at with zero value
		// when bp is a fresh struct from request payload.
		updates := db.BranchProtection{
			RequiredStatusChecksJSON:                   bp.RequiredStatusChecksJSON,
			ResolvedRequiredStatusChecksJSON:           resolvedChecks,
			RequiredStatusChecksProviderBindingPending: bindingPending,
			EnforceAdmins:                              bp.EnforceAdmins,
			RequiredSignatures:                         bp.RequiredSignatures,
			RequiredPullRequestJSON:                    bp.RequiredPullRequestJSON,
			RestrictionsJSON:                           bp.RestrictionsJSON,
		}
		if err := s.DBForCtx(ctx).Model(&existing).
			Select("RequiredStatusChecksJSON", "ResolvedRequiredStatusChecksJSON", "RequiredStatusChecksProviderBindingPending", "EnforceAdmins", "RequiredSignatures", "RequiredPullRequestJSON", "RestrictionsJSON").
			Updates(updates).Error; err != nil {
			return err
		}
		bp.ResolvedRequiredStatusChecksJSON = resolvedChecks
		bp.RequiredStatusChecksProviderBindingPending = bindingPending
		bp.ID = existing.ID
		bp.CreatedAt = existing.CreatedAt
		bp.UpdatedAt = existing.UpdatedAt
		return nil
	}

	if !errors.Is(err, gorm.ErrRecordNotFound) {
		return err
	}

	resolvedChecks, err := s.resolveRequiredStatusCheckProviders(ctx, bp.RepositoryID, bp.RequiredStatusChecksJSON)
	if err != nil {
		return err
	}
	bp.ResolvedRequiredStatusChecksJSON = resolvedChecks
	bp.RequiredStatusChecksProviderBindingPending = false
	// Create
	return s.DBForCtx(ctx).Create(bp).Error
}

// resolveRequiredStatusCheckProviders captures GitHub's write-time app
// selection semantics without rewriting the public request/response JSON.
// Omitted app_id (including legacy contexts) selects the most recent App that
// provided the context; explicit -1 remains the any-provider sentinel.
func (s *Service) resolveRequiredStatusCheckProviders(ctx context.Context, repositoryID uint, raw string) (string, error) {
	resolved, _, err := s.resolveRequiredStatusCheckProvidersAt(ctx, repositoryID, raw, nil, false)
	return resolved, err
}

func (s *Service) resolveRequiredStatusCheckProvidersAt(ctx context.Context, repositoryID uint, raw string, cutoff *time.Time, failClosed bool) (string, bool, error) {
	if strings.TrimSpace(raw) == "" {
		return "", false, nil
	}
	required, err := decodeBranchProtectionRequiredStatusChecks(raw)
	if err != nil {
		return "", false, err
	}
	resolved := branchProtectionRequiredStatusChecks{Strict: required.Strict}
	bindingPending := false
	resolve := func(name string, appID *int64) error {
		name = strings.TrimSpace(name)
		if name == "" {
			return nil
		}
		if appID == nil {
			selected, err := s.mostRecentStatusCheckProviderAt(ctx, repositoryID, name, cutoff)
			if err != nil {
				return err
			}
			if selected == nil {
				wildcard := int64(-1)
				selected = &wildcard
				bindingPending = bindingPending || failClosed
			}
			appID = selected
		}
		resolved.Checks = append(resolved.Checks, branchProtectionCheckEntry{Context: name, AppID: appID})
		return nil
	}
	checkNames := make(map[string]struct{}, len(required.Checks))
	for _, check := range required.Checks {
		name := check.Context
		if strings.TrimSpace(name) == "" {
			name = check.Name
		}
		if err := resolve(name, check.AppID); err != nil {
			return "", false, err
		}
		if name = strings.TrimSpace(name); name != "" {
			checkNames[name] = struct{}{}
		}
	}
	for _, name := range required.Contexts {
		if _, represented := checkNames[strings.TrimSpace(name)]; represented {
			continue
		}
		if err := resolve(name, nil); err != nil {
			return "", false, err
		}
	}
	encoded, err := json.Marshal(resolved)
	if err != nil {
		return "", false, err
	}
	return string(encoded), bindingPending, nil
}

func (s *Service) mostRecentStatusCheckProviderAt(ctx context.Context, repositoryID uint, name string, cutoff *time.Time) (*int64, error) {
	type providerCandidate struct {
		IntegrationID *int64
		SelectedAt    time.Time `gorm:"column:selected_at"`
	}
	timestampColumn := "updated_at"
	if cutoff != nil {
		// created_at proves that the provider evidence existed at the original
		// protection write. updated_at may reflect a later workflow completion.
		timestampColumn = "created_at"
	}
	var workflowCandidate providerCandidate
	workflowQuery := s.DBForCtx(ctx).Model(&db.WorkflowRun{}).
		Select("integration_id", timestampColumn+" AS selected_at").
		Where("repository_id = ? AND name = ? AND integration_id IS NOT NULL", repositoryID, name)
	if cutoff != nil {
		workflowQuery = workflowQuery.Where("created_at <= ?", *cutoff)
	}
	workflowErr := workflowQuery.Order(timestampColumn + " DESC").First(&workflowCandidate).Error
	if workflowErr != nil && !errors.Is(workflowErr, gorm.ErrRecordNotFound) {
		return nil, workflowErr
	}
	var statusCandidate providerCandidate
	statusQuery := s.DBForCtx(ctx).Model(&db.CommitStatus{}).
		Select("integration_id", timestampColumn+" AS selected_at").
		Where("repository_id = ? AND context = ? AND integration_id IS NOT NULL", repositoryID, name)
	if cutoff != nil {
		statusQuery = statusQuery.Where("created_at <= ?", *cutoff)
	}
	statusErr := statusQuery.Order(timestampColumn + " DESC").First(&statusCandidate).Error
	if statusErr != nil && !errors.Is(statusErr, gorm.ErrRecordNotFound) {
		return nil, statusErr
	}
	if workflowCandidate.IntegrationID == nil {
		return statusCandidate.IntegrationID, nil
	}
	if statusCandidate.IntegrationID == nil || !statusCandidate.SelectedAt.After(workflowCandidate.SelectedAt) {
		return workflowCandidate.IntegrationID, nil
	}
	return statusCandidate.IntegrationID, nil
}

// migrateLegacyRequiredStatusCheckProviders upgrades rows written before the
// internal binding snapshot existed. Provider selection is reconstructed only
// from evidence that predates the protection write. Missing evidence is not
// silently converted to an any-provider rule: policy stays fail-closed until
// a fresh PUT applies GitHub's current write-time semantics.
func (s *Service) migrateLegacyRequiredStatusCheckProviders(ctx context.Context, bp *db.BranchProtection) error {
	if bp == nil || strings.TrimSpace(bp.RequiredStatusChecksJSON) == "" || strings.TrimSpace(bp.ResolvedRequiredStatusChecksJSON) != "" {
		return nil
	}
	resolved, pending, err := s.resolveRequiredStatusCheckProvidersAt(ctx, bp.RepositoryID, bp.RequiredStatusChecksJSON, &bp.UpdatedAt, true)
	if err != nil {
		return fmt.Errorf("%w: invalid required status checks configuration", ErrInvalidState)
	}
	result := s.DBForCtx(ctx).Model(&db.BranchProtection{}).
		Where("id = ? AND resolved_required_status_checks_json = ?", bp.ID, "").
		UpdateColumns(map[string]any{
			"resolved_required_status_checks_json":            resolved,
			"required_status_checks_provider_binding_pending": pending,
		})
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return s.DBForCtx(ctx).First(bp, bp.ID).Error
	}
	bp.ResolvedRequiredStatusChecksJSON = resolved
	bp.RequiredStatusChecksProviderBindingPending = pending
	return nil
}

// DeleteBranchProtection removes the branch protection rules for a branch.
func (s *Service) DeleteBranchProtection(ctx context.Context, repoID uint, branch string) error {
	return s.DBForCtx(ctx).
		Where("repository_id = ? AND branch_name = ?", repoID, branch).
		Delete(&db.BranchProtection{}).Error
}
