package repocache

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	gogit "github.com/go-git/go-git/v5"
	gitconfig "github.com/go-git/go-git/v5/config"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/transport"
	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	store "github.com/tidbcloud/AgentTeam/internal/store/repocache"
)

const (
	retiredGenerationBuffer = 15 * time.Minute
	defaultCleanupBatch     = 50
	maxCleanupBatch         = 100
)

type buildResult struct {
	Generation string
	Commit     string
	SizeBytes  int64
}

func (s *Service) buildGeneration(ctx context.Context, cache store.Cache) (buildResult, error) {
	if strings.TrimSpace(s.root) == "" {
		return buildResult{}, fmt.Errorf("public repository cache root is not configured")
	}
	if err := os.MkdirAll(s.root, 0o700); err != nil {
		return buildResult{}, fmt.Errorf("prepare public repository cache root: %w", err)
	}
	cacheID, err := safePathComponent(cache.ID)
	if err != nil {
		return buildResult{}, fmt.Errorf("invalid public repository cache id: %w", err)
	}
	cmdCtx, cancel := context.WithTimeout(ctx, s.buildTimeout)
	defer cancel()
	generation, err := identifiers.NewEntityID("prcg")
	if err != nil {
		return buildResult{}, fmt.Errorf("create public repository cache generation id: %w", err)
	}
	tempPath, err := os.MkdirTemp(s.root, ".build-"+cacheID+"-")
	if err != nil {
		return buildResult{}, err
	}
	defer os.RemoveAll(tempPath)
	repoPath := filepath.Join(tempPath, "repo.git")
	repo, err := s.cloneGeneration(cmdCtx, cache, repoPath)
	if err != nil {
		return buildResult{}, err
	}
	sizeBytes, err := directoryFileSize(cmdCtx, repoPath)
	if err != nil {
		return buildResult{}, fmt.Errorf("measure public repository cache: %w", err)
	}
	commit := mirrorHEAD(repo)
	finalPath, err := s.generationPath(cache.ID, generation)
	if err != nil {
		return buildResult{}, err
	}
	if err := os.MkdirAll(filepath.Dir(finalPath), 0o700); err != nil {
		return buildResult{}, fmt.Errorf("prepare public repository cache versions: %w", err)
	}
	if err := os.Rename(repoPath, finalPath); err != nil {
		return buildResult{}, fmt.Errorf("publish public repository cache generation: %w", err)
	}
	return buildResult{Generation: generation, Commit: commit, SizeBytes: sizeBytes}, nil
}

func (s *Service) cloneGeneration(ctx context.Context, cache store.Cache, repoPath string) (*gogit.Repository, error) {
	if sourcePath, err := s.servingPath(cache); err != nil {
		return nil, err
	} else if _, err := os.Stat(sourcePath); err == nil {
		// Keep Shared=false so the new generation owns its objects and remains
		// valid after the retired source generation is garbage-collected.
		repo, err := gogit.PlainCloneContext(ctx, repoPath, true, &gogit.CloneOptions{URL: sourcePath, Mirror: true})
		if err != nil {
			return nil, fmt.Errorf("seed public repository cache generation: %w", err)
		}
		if err := setMirrorOrigin(repo, cache.NormalizedRemoteURL); err != nil {
			return nil, fmt.Errorf("configure public repository cache generation: %w", err)
		}
		if err := fetchMirror(ctx, repo, s.proxyURL); err != nil {
			return nil, fmt.Errorf("refresh public repository cache generation: %w", err)
		}
		return repo, nil
	} else if !os.IsNotExist(err) {
		return nil, fmt.Errorf("inspect public repository cache generation: %w", err)
	}

	repo, err := gogit.PlainCloneContext(ctx, repoPath, true, &gogit.CloneOptions{URL: cache.NormalizedRemoteURL, Mirror: true, ProxyOptions: proxyOptions(s.proxyURL)})
	if err != nil {
		return nil, fmt.Errorf("clone public repository cache generation: %w", err)
	}
	return repo, nil
}

func setMirrorOrigin(repo *gogit.Repository, remoteURL string) error {
	if err := repo.DeleteRemote("origin"); err != nil {
		return err
	}
	_, err := repo.CreateRemote(&gitconfig.RemoteConfig{
		Name:  "origin",
		URLs:  []string{strings.TrimSpace(remoteURL)},
		Fetch: []gitconfig.RefSpec{"+refs/*:refs/*"},
	})
	return err
}

func directoryFileSize(ctx context.Context, root string) (int64, error) {
	var total int64
	err := filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if err := ctx.Err(); err != nil {
			return err
		}
		if !entry.Type().IsRegular() {
			return nil
		}
		info, err := entry.Info()
		if err != nil {
			return err
		}
		total += info.Size()
		return nil
	})
	return total, err
}

func fetchMirror(ctx context.Context, repo *gogit.Repository, proxyURL string) error {
	remote, err := repo.Remote("origin")
	if err != nil {
		return err
	}
	err = remote.FetchContext(ctx, &gogit.FetchOptions{Force: true, Prune: true, Tags: gogit.AllTags, ProxyOptions: proxyOptions(proxyURL)})
	if errors.Is(err, gogit.NoErrAlreadyUpToDate) {
		return nil
	}
	return err
}

func mirrorHEAD(repo *gogit.Repository) string {
	if ref, err := repo.Reference(plumbing.HEAD, true); err == nil {
		return ref.Hash().String()
	}
	return ""
}

func proxyOptions(raw string) transport.ProxyOptions {
	if strings.TrimSpace(raw) == "" {
		return transport.ProxyOptions{}
	}
	return transport.ProxyOptions{URL: strings.TrimSpace(raw)}
}

func (s *Service) servingPath(cache store.Cache) (string, error) {
	if strings.TrimSpace(cache.ActiveGeneration) == "" {
		return s.legacyCachePath(cache.ID)
	}
	return s.generationPath(cache.ID, cache.ActiveGeneration)
}

func (s *Service) legacyCachePath(id string) (string, error) {
	cacheID, err := safePathComponent(id)
	if err != nil {
		return "", fmt.Errorf("invalid public repository cache id: %w", err)
	}
	return filepath.Join(s.root, cacheID+".git"), nil
}

func (s *Service) cacheRoot(id string) (string, error) {
	cacheID, err := safePathComponent(id)
	if err != nil {
		return "", fmt.Errorf("invalid public repository cache id: %w", err)
	}
	return filepath.Join(s.root, cacheID), nil
}

func (s *Service) generationPath(id, generation string) (string, error) {
	root, err := s.cacheRoot(id)
	if err != nil {
		return "", err
	}
	generation, err = safePathComponent(generation)
	if err != nil {
		return "", fmt.Errorf("invalid public repository cache generation: %w", err)
	}
	return filepath.Join(root, "versions", generation+".git"), nil
}

func safePathComponent(value string) (string, error) {
	value = strings.TrimSpace(value)
	if value == "" || value == "." || value == ".." || filepath.Base(value) != value || strings.ContainsAny(value, `/\`) {
		return "", fmt.Errorf("unsafe path component")
	}
	return value, nil
}

func (s *Service) markServingGenerationRetired(cache store.Cache, retiredAt time.Time) error {
	path, err := s.servingPath(cache)
	if err != nil {
		return err
	}
	if _, err := os.Stat(path); err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return fmt.Errorf("inspect previous public repository cache generation: %w", err)
	}
	marker, err := s.retirementMarkerPath(cache.ID, cache.ActiveGeneration)
	if err != nil {
		return err
	}
	if err := os.MkdirAll(filepath.Dir(marker), 0o700); err != nil {
		return fmt.Errorf("prepare public repository cache retirement marker: %w", err)
	}
	if err := os.WriteFile(marker, nil, 0o600); err != nil {
		return fmt.Errorf("mark previous public repository cache generation retired: %w", err)
	}
	if err := os.Chtimes(marker, retiredAt, retiredAt); err != nil {
		return fmt.Errorf("timestamp previous public repository cache generation retirement: %w", err)
	}
	return nil
}

func (s *Service) retirementMarkerPath(id, generation string) (string, error) {
	root, err := s.cacheRoot(id)
	if err != nil {
		return "", err
	}
	name := "legacy"
	if strings.TrimSpace(generation) != "" {
		name, err = safePathComponent(generation)
		if err != nil {
			return "", fmt.Errorf("invalid public repository cache generation: %w", err)
		}
	}
	return filepath.Join(root, "retired", name), nil
}

// CleanupStaleGenerations advances one bounded, ID-ordered sweep page. The
// in-memory cursor wraps after the last page, so the periodic worker eventually
// revisits every cache without scanning the full registry in one tick.
func (s *Service) CleanupStaleGenerations(ctx context.Context, limit int) (int, error) {
	if limit <= 0 || limit > maxCleanupBatch {
		limit = defaultCleanupBatch
	}
	s.cleanupMu.Lock()
	defer s.cleanupMu.Unlock()

	caches, err := s.store.ListByIDAfter(ctx, s.cleanupAfter, limit)
	if err != nil {
		return 0, err
	}
	if len(caches) == 0 {
		s.cleanupAfter = ""
		return 0, nil
	}

	now := s.now()
	processed := 0
	var cleanupErr error
	for _, cache := range caches {
		if err := ctx.Err(); err != nil {
			cleanupErr = errors.Join(cleanupErr, err)
			break
		}
		s.cleanupAfter = cache.ID
		processed++
		if err := s.cleanupRetiredGenerations(cache.ID, cache.ActiveGeneration, now); err != nil {
			cleanupErr = errors.Join(cleanupErr, fmt.Errorf("clean up public repository cache %s: %w", cache.ID, err))
		}
	}
	if processed == len(caches) && len(caches) < limit {
		s.cleanupAfter = ""
	}
	return processed, cleanupErr
}

func (s *Service) cleanupRetiredGenerations(id, activeGeneration string, now time.Time) error {
	root, err := s.cacheRoot(id)
	if err != nil {
		return err
	}
	versionsRoot := filepath.Join(root, "versions")
	entries, err := os.ReadDir(versionsRoot)
	if err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("list public repository cache generations: %w", err)
	}
	retiredCutoff := now.Add(-(s.buildTimeout + retiredGenerationBuffer))
	// An unmarked generation may have been renamed by an attempt that has not
	// reached its lease-fenced DB switch yet. Keep it beyond the maximum lease;
	// CompleteBuild rejects the attempt after that lease expires.
	orphanCutoff := now.Add(-(buildLeaseDuration(s.buildTimeout) + retiredGenerationBuffer))
	activeGeneration = strings.TrimSpace(activeGeneration)
	for _, entry := range entries {
		if !entry.IsDir() || !strings.HasSuffix(entry.Name(), ".git") {
			continue
		}
		generation := strings.TrimSuffix(entry.Name(), ".git")
		if generation == activeGeneration {
			continue
		}
		generationPath, err := s.generationPath(id, generation)
		if err != nil {
			return err
		}
		marker, err := s.retirementMarkerPath(id, generation)
		if err != nil {
			return err
		}
		retiredAt, marked, err := retirementTime(marker, generationPath)
		if err != nil {
			return err
		}
		cutoff := orphanCutoff
		if marked {
			cutoff = retiredCutoff
		}
		if retiredAt.After(cutoff) {
			continue
		}
		if err := os.RemoveAll(generationPath); err != nil {
			return fmt.Errorf("remove retired public repository cache generation: %w", err)
		}
		if err := os.Remove(marker); err != nil && !os.IsNotExist(err) {
			return fmt.Errorf("remove public repository cache retirement marker: %w", err)
		}
	}

	legacyPath, err := s.legacyCachePath(id)
	if err != nil {
		return err
	}
	legacyMarker, err := s.retirementMarkerPath(id, "")
	if err != nil {
		return err
	}
	if activeGeneration != "" {
		if _, err := os.Stat(legacyPath); err == nil {
			retiredAt, _, err := retirementTime(legacyMarker, legacyPath)
			if err != nil {
				return err
			}
			if !retiredAt.After(retiredCutoff) {
				if err := os.RemoveAll(legacyPath); err != nil {
					return fmt.Errorf("remove retired legacy public repository cache: %w", err)
				}
				if err := os.Remove(legacyMarker); err != nil && !os.IsNotExist(err) {
					return fmt.Errorf("remove legacy public repository cache retirement marker: %w", err)
				}
			}
		} else if !os.IsNotExist(err) {
			return fmt.Errorf("inspect legacy public repository cache: %w", err)
		}
	}
	return nil
}

func retirementTime(markerPath, generationPath string) (time.Time, bool, error) {
	if info, err := os.Stat(markerPath); err == nil {
		return info.ModTime(), true, nil
	} else if !os.IsNotExist(err) {
		return time.Time{}, false, fmt.Errorf("inspect public repository cache retirement marker: %w", err)
	}
	info, err := os.Stat(generationPath)
	if err != nil {
		return time.Time{}, false, fmt.Errorf("inspect public repository cache generation: %w", err)
	}
	return info.ModTime(), false, nil
}
