package service

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

	"github.com/go-git/go-git/v5/plumbing"
	"github.com/pingcap/agent-git-service/internal/db"
	"github.com/pingcap/agent-git-service/internal/gitstore"
	"github.com/pingcap/agent-git-service/internal/wikicatalog"
)

// Legacy changesets lack durable prepared objects. Recover their active chain
// in order from revision bodies, never from a page's newer live inline body.
// Superseded history belongs to compaction, not the publication backlog.
func (s *Service) recoverLegacyWikiProjectionsLocked(ctx context.Context, git *gitstore.Store, repo db.Repository) (<-chan error, error) {
	var pending []db.WikiChangeset
	if err := s.DBForCtx(ctx).
		Where("repository_id = ? AND synth_format_ver < ? AND superseded_by_changeset_id IS NULL", repo.ID, synthProjectionMaterialized).
		Where("source IN ?", []string{string(wikicatalog.SourceREST), string(wikicatalog.SourceAdmin), string(wikicatalog.SourceBatch)}).
		Order("changeset_id ASC").Find(&pending).Error; err != nil {
		return nil, err
	}
	var done <-chan error
	for _, changeset := range pending {
		next, err := s.recoverWikiChangesetChainLocked(ctx, git, repo, changeset, make(map[uint64]bool))
		done = combineWikiPostCommitWaiters(done, next)
		if err != nil {
			return done, err
		}
	}
	return done, nil
}

func (s *Service) recoverWikiChangesetChainLocked(ctx context.Context, git *gitstore.Store, repo db.Repository, changeset db.WikiChangeset, visiting map[uint64]bool) (<-chan error, error) {
	if visiting[changeset.ChangesetID] {
		return nil, fmt.Errorf("%w: cyclic wiki projection history at changeset %d", ErrConflict, changeset.ChangesetID)
	}
	visiting[changeset.ChangesetID] = true
	defer delete(visiting, changeset.ChangesetID)
	full := wikiRepoFullName(repo.FullName)
	current, err := git.HeadSHA(ctx, full, wikiDefaultBranch)
	if err != nil && !errors.Is(err, plumbing.ErrReferenceNotFound) {
		return nil, err
	}
	if current != "" {
		published, err := git.IsAncestor(ctx, full, changeset.SynthCommitSHA, current)
		if err != nil && !errors.Is(err, plumbing.ErrObjectNotFound) {
			return nil, err
		}
		if published {
			// A historical marker can lag publication. Never move the ref back
			// or replay stale page effects when Git proves it already landed.
			if changeset.SynthFormatVer < synthProjectionMaterialized {
				return nil, s.markWikiChangesetMaterialized(ctx, changeset.ChangesetID, changeset.SynthCommitSHA)
			}
			return nil, nil
		}
	}
	if !wikiChangesetNeedsGitProjection(wikicatalog.Source(changeset.Source)) {
		return nil, fmt.Errorf("%w: wiki projection %s cannot replay changeset %d from source %q", ErrConflict, repo.FullName, changeset.ChangesetID, changeset.Source)
	}
	var done <-chan error
	parentSHA := ""
	if changeset.ParentID != nil {
		parent, err := s.loadWikiProjectionParent(ctx, git, repo, *changeset.ParentID)
		if err != nil {
			return nil, err
		}
		done, err = s.recoverWikiChangesetChainLocked(ctx, git, repo, parent, visiting)
		if err != nil {
			return done, err
		}
		// Recovery may replace a legacy synthetic identity with its real Git SHA.
		if err := s.DBForCtx(ctx).Where("changeset_id = ?", parent.ChangesetID).Take(&parent).Error; err != nil {
			return done, err
		}
		parentSHA = parent.SynthCommitSHA
	}
	current, err = git.HeadSHA(ctx, full, wikiDefaultBranch)
	if err != nil && !errors.Is(err, plumbing.ErrReferenceNotFound) {
		return done, err
	}
	if !strings.EqualFold(current, parentSHA) {
		return done, fmt.Errorf("%w: wiki projection %s diverged before changeset %d: git=%s parent=%s", ErrConflict, repo.FullName, changeset.ChangesetID, current, parentSHA)
	}
	next, err := s.resumePendingWikiChangesetProjectionLocked(ctx, git, repo, changeset)
	if errors.Is(err, gitstore.ErrRefChanged) {
		return done, fmt.Errorf("%w: wiki projection changed while recovering %s: %v", ErrConflict, repo.FullName, err)
	}
	if !errors.Is(err, plumbing.ErrObjectNotFound) || changeset.SynthFormatVer >= synthProjectionMaterialized {
		return combineWikiPostCommitWaiters(done, next), err
	}
	result, err := s.loadWikiChangesetResult(ctx, changeset)
	if err != nil {
		return done, err
	}
	mutations := make([]gitstore.FileMutation, 0, len(result.Changes)*2)
	for _, change := range result.Changes {
		if change.Op == wikicatalog.OpDelete || change.Op == wikicatalog.OpRename {
			mutations = append(mutations, gitstore.FileMutation{Path: wikiSlugToPath(change.PrevSlug), Delete: true})
		}
		if change.Op != wikicatalog.OpDelete {
			mutations = append(mutations, gitstore.FileMutation{Path: wikiSlugToPath(change.Slug), Content: change.Body})
		}
	}
	prepared, err := git.BuildCommitFilesAtParent(ctx, full, wikiDefaultBranch, plumbing.NewHash(parentSHA).String(), result.Message, mutations, result.CommittedAt)
	if err != nil {
		return done, err
	}
	if err := git.PersistPreparedCommit(ctx, prepared); err != nil {
		return done, err
	}
	// Persist the exact recovery identity before publishing, just like a new
	// REST write. A crash from this point is an ordinary prepared-ref recovery.
	if err := s.markWikiChangesetMaterialized(ctx, changeset.ChangesetID, prepared.SHA); err != nil {
		return done, err
	}
	changeset.SynthCommitSHA = prepared.SHA
	changeset.SynthFormatVer = synthProjectionMaterialized
	next, err = s.resumePendingWikiChangesetProjectionLocked(ctx, git, repo, changeset)
	return combineWikiPostCommitWaiters(done, next), err
}

// Compaction records a catalog snapshot on a separate Git ref; it need not be
// an ancestor of the content branch. Follow its durable parent to find the
// publication base, unless an older deployment published the compact commit
// onto the content branch itself. Neither case requires rewriting that branch.
func (s *Service) loadWikiProjectionParent(ctx context.Context, git *gitstore.Store, repo db.Repository, id uint64) (db.WikiChangeset, error) {
	seen := make(map[uint64]bool)
	for {
		if seen[id] {
			return db.WikiChangeset{}, fmt.Errorf("%w: cyclic wiki compaction history at %d", ErrConflict, id)
		}
		seen[id] = true
		var parent db.WikiChangeset
		if err := s.DBForCtx(ctx).Where("repository_id = ? AND changeset_id = ?", repo.ID, id).Take(&parent).Error; err != nil {
			return parent, fmt.Errorf("load wiki projection parent: %w", err)
		}
		if wikicatalog.Source(parent.Source) != wikicatalog.SourceCompact {
			return parent, nil
		}
		if parent.SynthFormatVer < synthProjectionMaterialized {
			if err := s.resumePendingWikiCompactProjection(ctx, repo.FullName, parent); err != nil {
				return parent, err
			}
		}
		current, err := git.HeadSHA(ctx, wikiRepoFullName(repo.FullName), wikiDefaultBranch)
		if err != nil {
			return parent, err
		}
		published, err := git.IsAncestor(ctx, wikiRepoFullName(repo.FullName), parent.SynthCommitSHA, current)
		if err != nil {
			return parent, err
		}
		if published {
			return parent, nil
		}
		if parent.ParentID == nil {
			return parent, fmt.Errorf("%w: wiki compact changeset %d has no content-branch base", ErrConflict, id)
		}
		id = *parent.ParentID
	}
}
