package service

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"strings"

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

// ResolveForkRepo finds the fork of baseFullName owned by forkOwner.
// It first tries forkOwner/baseName directly, then searches for any repo
// owned by forkOwner that is a fork of the base. Returns "" if not found.
func (s *Service) ResolveForkRepo(ctx context.Context, baseFullName, forkOwner string) string {
	parts := strings.SplitN(baseFullName, "/", 2)
	if len(parts) != 2 {
		return ""
	}
	directName := forkOwner + "/" + parts[1]
	if _, err := s.GetRepo(ctx, directName); err == nil {
		return directName
	}
	repos, _ := s.ListUserRepos(ctx, forkOwner)
	baseRepo, _ := s.GetRepo(ctx, baseFullName)
	for _, r := range repos {
		if r.Fork && r.ParentID != nil && *r.ParentID == baseRepo.ID {
			return r.FullName
		}
	}
	return ""
}

// ForkRepo creates a fork of a repository under the given owner.
func (s *Service) ForkRepo(ctx context.Context, sourceFullName, targetOwnerLogin, targetName string) (db.Repository, error) {
	src, err := s.GetRepo(ctx, sourceFullName)
	if err != nil {
		return db.Repository{}, err
	}
	targetOwner, err := s.GetUser(ctx, targetOwnerLogin)
	if err != nil {
		return db.Repository{}, err
	}
	targetsOrg := targetOwner.Type == db.TypeOrganization
	if targetsOrg {
		if err := s.requireOrgAdminForRepoTarget(ctx, targetOwner.ID); err != nil {
			return db.Repository{}, err
		}
	}

	name := src.Name
	if targetName != "" {
		name = targetName
	}

	in := CreateRepoInput{
		OwnerLogin:       targetOwnerLogin,
		Name:             name,
		Description:      src.Description,
		Private:          src.Private,
		DefaultBranch:    src.DefaultBranch,
		RequireOrgAdmin:  targetsOrg,
		SkipOrgBootstrap: targetsOrg,
	}
	fork, err := s.CreateRepo(ctx, in)
	if err != nil {
		return db.Repository{}, err
	}

	mutationErr := func() error {
		session, err := s.repositoryMutations.AcquireSession(ctx, fork.ID, fork.FullName)
		if err != nil {
			return repositoryMutationError(err)
		}
		defer func() { _ = session.Release() }()
		if err := session.ForkFrom(ctx, src.FullName); err != nil {
			return fmt.Errorf("%w: %v", ErrValidation, err)
		}
		// Finalize fork metadata while the target source session remains held.
		if err := s.DBForCtx(ctx).Transaction(func(tx *gorm.DB) error {
			return tx.Model(&db.Repository{}).
				Where("id = ?", fork.ID).
				Updates(map[string]any{"fork": true, "parent_id": src.ID}).Error
		}); err != nil {
			return err
		}
		return nil
	}()
	if mutationErr != nil {
		stage := "git_fork_failed"
		finalizeFailed := !errors.Is(mutationErr, ErrValidation)
		if finalizeFailed {
			stage = "db_finalize_failed"
			slog.Error("ForkRepo: finalize metadata failed; compensating", "repo", fork.FullName, "error", mutationErr)
		}
		if cleanupErr := s.cleanupForkRepo(ctx, fork.FullName, stage); cleanupErr != nil {
			// A compensation failure is an internal partial-failure state even
			// when the initiating mutation was a validation error. Strip only that
			// surface sentinel; raw DB and Git errors retain their unwrap chains.
			joinedMutationErr := mutationErr
			if errors.Is(mutationErr, ErrValidation) {
				joinedMutationErr = errors.New(mutationErr.Error())
			}
			return db.Repository{}, errors.Join(joinedMutationErr, fmt.Errorf("cleanup failed: %w", cleanupErr))
		}
		if finalizeFailed {
			return db.Repository{}, fmt.Errorf("%w: %v", ErrValidation, mutationErr)
		}
		return db.Repository{}, mutationErr
	}

	if err := preloadRepoMinimal(s.DBForCtx(ctx)).First(&fork, fork.ID).Error; err != nil {
		return fork, wrapErr(err)
	}
	return fork, nil
}

func (s *Service) cleanupForkRepo(ctx context.Context, fullName, stage string) error {
	if err := s.DeleteRepo(ctx, fullName); err != nil {
		slog.Error("ForkRepo: cleanup repo failed", "repo", fullName, "stage", stage, "error", err)
		return err
	}
	return nil
}

// ListForks returns all repositories forked from the given repository ID.
func (s *Service) ListForks(ctx context.Context, repoID uint) ([]db.Repository, error) {
	var forks []db.Repository
	if err := s.DBForCtx(ctx).Preload("Owner").Where("parent_id = ?", repoID).Find(&forks).Error; err != nil {
		return nil, err
	}
	return forks, nil
}

// ForkCount returns the number of forks for a repository.
func (s *Service) ForkCount(ctx context.Context, repoID uint) int {
	var count int64
	if err := s.DBForCtx(ctx).Model(&db.Repository{}).Where("parent_id = ?", repoID).Count(&count).Error; err != nil {
		slog.Error("ForkCount", "error", err)
	}
	return int(count)
}
