package service

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"path/filepath"
	"strings"
	"time"

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

	"gopkg.in/yaml.v3"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

// githubActionsIntegrationID is GitHub's public App identity for Actions.
// Persisting it on locally executed workflow runs lets provider-bound required
// checks distinguish Actions from user-created statuses with the same name.
const githubActionsIntegrationID int64 = 15368

func githubActionsIntegrationIDPtr() *int64 {
	id := githubActionsIntegrationID
	return &id
}

// DispatchWorkflow dispatches a workflow, executes its steps, and returns the created run.
func (s *Service) DispatchWorkflow(ctx context.Context, repoFullName string, workflowID int, ref string, inputs map[string]any) (db.WorkflowRun, error) {
	repo, err := s.GetRepo(ctx, repoFullName)
	if err != nil {
		return db.WorkflowRun{}, err
	}

	var wf db.Workflow
	if err := s.DBForCtx(ctx).Where("repository_id = ? AND id = ?", repo.ID, workflowID).First(&wf).Error; err != nil {
		return db.WorkflowRun{}, wrapErr(err)
	}

	if wf.State != db.WorkflowActive {
		return db.WorkflowRun{}, fmt.Errorf("%w: workflow is disabled", ErrInvalidState)
	}
	if err := s.validateWorkflowDispatchEligibility(ctx, repo, wf); err != nil {
		return db.WorkflowRun{}, err
	}

	var count int64
	if err := s.DBForCtx(ctx).Model(&db.WorkflowRun{}).Where("workflow_id = ?", wf.ID).Count(&count).Error; err != nil {
		return db.WorkflowRun{}, fmt.Errorf("dispatch workflow: count runs: %w", err)
	}

	// Resolve ref to SHA before creating the run.
	headSHA, err := s.resolveRefSHA(ctx, repoFullName, ref)
	if err != nil {
		return db.WorkflowRun{}, err
	}
	gitStore, err := s.GitForCtx(ctx)
	if err != nil {
		return db.WorkflowRun{}, err
	}
	definition, err := gitStore.ReadFileAtRef(ctx, repoFullName, wf.Path, headSHA)
	if err != nil {
		return db.WorkflowRun{}, fmt.Errorf("dispatch workflow: snapshot %s at %s: %w", wf.Path, headSHA, err)
	}

	runNum := int(count) + 1
	var actorID *uint
	if user, ok := UserFromContext(ctx); ok {
		actorID = &user.ID
	}

	run := db.WorkflowRun{
		RepositoryID:  repo.ID,
		WorkflowID:    wf.ID,
		ActorID:       actorID,
		IntegrationID: githubActionsIntegrationIDPtr(),
		Name: func() string {
			if wf.Name != "" {
				return wf.Name
			}
			return fmt.Sprintf("Run %d", runNum)
		}(),
		WorkflowPath: wf.Path,
		HeadBranch:   ref,
		HeadSHA:      headSHA,
		RunNumber:    runNum,
		RunAttempt:   1,
		Event:        "workflow_dispatch",
		Status:       db.RunQueued,
		Conclusion:   "",
	}

	if _, err := s.createWorkflowRunWithSnapshot(ctx, &run, definition, false); err != nil {
		return db.WorkflowRun{}, err
	}

	// Create a cache entry for cache tests.
	s.DBForCtx(ctx).Create(&db.ActionCache{
		RepositoryID:   repo.ID,
		Key:            "Linux-values",
		Ref:            gitstore.RefsHeadsPrefix + repo.DefaultBranch,
		Version:        "sha-version",
		SizeInBytes:    1024,
		LastAccessedAt: time.Now(),
	})

	// The row is the durable queue. Kick an executor immediately; the recovery
	// loop will reclaim it if the process exits anywhere after Create.
	if err := s.claimAndStartWorkflowRun(ctx, run.ID); err != nil {
		slog.WarnContext(ctx, "workflow execution kickoff failed", "workflow_run_id", run.ID, "error", err)
	}

	return run, nil
}

// validateWorkflowDispatchEligibility treats the default-branch definition as
// the authorization source for manual dispatch. The selected ref is resolved
// and snapshotted separately by DispatchWorkflow after this check succeeds.
func (s *Service) validateWorkflowDispatchEligibility(ctx context.Context, repo db.Repository, workflow db.Workflow) error {
	gitStore, err := s.GitForCtx(ctx)
	if err != nil {
		return err
	}
	defaultSHA, err := s.resolveRefSHA(ctx, repo.FullName, repo.DefaultBranch)
	if err != nil {
		return fmt.Errorf("dispatch workflow: resolve default branch: %w", err)
	}
	definition, err := gitStore.ReadFileAtRef(ctx, repo.FullName, workflow.Path, defaultSHA)
	if err != nil {
		if errors.Is(err, gitstore.ErrContentNotFound) || errors.Is(err, gitstore.ErrCommitNotFound) {
			return fmt.Errorf("%w: workflow does not exist on the default branch", ErrInvalidState)
		}
		return fmt.Errorf("dispatch workflow: read default-branch definition: %w", err)
	}
	if !workflowYAMLDeclaresEvent(definition, "workflow_dispatch") {
		return fmt.Errorf("%w: workflow does not have a workflow_dispatch trigger", ErrInvalidState)
	}
	return nil
}

// createWorkflowRunWithSnapshot persists the durable queue row and immutable
// definition atomically. When idempotent is true, a uniqueness conflict is
// reported as created=false so the caller can load the existing logical run.
func (s *Service) createWorkflowRunWithSnapshot(ctx context.Context, run *db.WorkflowRun, definition []byte, idempotent bool) (created bool, err error) {
	err = s.DBForCtx(ctx).Transaction(func(tx *gorm.DB) error {
		create := tx
		if idempotent {
			create = create.Clauses(clause.OnConflict{DoNothing: true})
		}
		result := create.Create(run)
		if result.Error != nil {
			return result.Error
		}
		if result.RowsAffected == 0 {
			return nil
		}
		created = true
		return tx.Create(&db.WorkflowRunSnapshot{
			WorkflowRunID: run.ID,
			Definition:    db.LargeText(definition),
		}).Error
	})
	return created, err
}

// ExtractWorkflowName extracts the workflow name from a workflow YAML file.
func ExtractWorkflowName(content []byte, filename string) string {
	var wf struct {
		Name string `yaml:"name"`
	}
	if yaml.Unmarshal(content, &wf) == nil && wf.Name != "" {
		return wf.Name
	}
	base := filepath.Base(filename)
	return strings.TrimSuffix(strings.TrimSuffix(base, ".yml"), ".yaml")
}

// SyncWorkflowsFromRepo parses workflow files from the repository and creates/updates them in the database.
func (s *Service) SyncWorkflowsFromRepo(ctx context.Context, repoFullName string) error {
	git, err := s.GitForCtx(ctx)
	if err != nil {
		return err
	}
	mu, err := s.getWorkflowSyncMu(ctx, repoFullName)
	if err != nil {
		return err
	}
	mu.Lock()
	defer mu.Unlock()

	repo, err := s.GetRepo(ctx, repoFullName)
	if err != nil {
		return err
	}

	files, err := git.ListTreeFiles(ctx, repoFullName)
	if err != nil {
		return nil // No workflows if no commits or tree
	}

	currentPaths := make(map[string]struct{})
	for _, f := range files {
		if !strings.HasPrefix(f, ".github/workflows/") || (!strings.HasSuffix(f, ".yml") && !strings.HasSuffix(f, ".yaml")) {
			continue
		}
		currentPaths[f] = struct{}{}
		content, err := git.ReadFile(ctx, repoFullName, f)
		if err != nil {
			continue
		}
		name := ExtractWorkflowName(content, f)

		var wf db.Workflow
		if s.DBForCtx(ctx).Where("repository_id = ? AND path = ?", repo.ID, f).First(&wf).Error == nil {
			if wf.Name != name {
				wf.Name = name
				if err := s.DBForCtx(ctx).Save(&wf).Error; err != nil {
					slog.ErrorContext(ctx, "sync workflows save failed", "file", f, "repo", repoFullName, "error", err)
				}
			}
			continue
		}
		if err := s.DBForCtx(ctx).Create(&db.Workflow{
			RepositoryID: repo.ID,
			Name:         name,
			Path:         f,
			State:        db.WorkflowActive,
		}).Error; err != nil {
			slog.ErrorContext(ctx, "sync workflows create failed", "file", f, "repo", repoFullName, "error", err)
		}
	}

	q := s.DBForCtx(ctx).Where("repository_id = ?", repo.ID)
	if len(currentPaths) == 0 {
		if err := q.Delete(&db.Workflow{}).Error; err != nil {
			slog.ErrorContext(ctx, "sync workflows delete failed", "repo", repoFullName, "error", err)
		}
		return nil
	}

	paths := make([]string, 0, len(currentPaths))
	for path := range currentPaths {
		paths = append(paths, path)
	}
	if err := q.Where("path NOT IN ?", paths).Delete(&db.Workflow{}).Error; err != nil {
		slog.ErrorContext(ctx, "sync workflows delete stale failed", "repo", repoFullName, "error", err)
	}

	return nil
}

// RerunWorkflowRun handles re-running a completed workflow run.
func (s *Service) RerunWorkflowRun(ctx context.Context, repoFullName string, runID int) error {
	repo, err := s.GetRepo(ctx, repoFullName)
	if err != nil {
		return err
	}

	var run db.WorkflowRun
	if err := s.DBForCtx(ctx).Where("repository_id = ? AND id = ?", repo.ID, runID).First(&run).Error; err != nil {
		return ErrNotFound
	}

	if err := s.DBForCtx(ctx).Model(&run).Updates(map[string]any{
		"status":      db.RunCompleted,
		"conclusion":  db.ConclusionSuccess,
		"run_attempt": run.RunAttempt + 1,
		"updated_at":  time.Now(),
	}).Error; err != nil {
		return err
	}
	if run.ActorID != nil {
		if _, err := s.CreateWorkflowEventNotification(ctx, *run.ActorID, *run.ActorID, run.ID, run.RepositoryID); err != nil {
			return err
		}
	}
	return nil
}
