package contentpayload

import (
	"context"
	"errors"
	"fmt"
	"io/fs"
	"log/slog"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/go-git/go-git/v5/plumbing"
	"github.com/tidbcloud/AgentTeam/internal/observability/logging"
)

const (
	DefaultGitMigrationMaxFiles = 1_000_000
	DefaultGitMigrationMaxBytes = int64(64 << 30)
	MaxGitMigrationProtectBatch = 100
	MaxGitMigrationConcurrency  = 4
	defaultGitImportTimeout     = 30 * time.Minute
	defaultGitVerifyTimeout     = 10 * time.Minute
	defaultGitProtectTimeout    = 2 * time.Minute
)

// ImportGitRepositoryRequest is an offline-admin operation. SourcePath is
// intentionally absent from the product payload interfaces.
type ImportGitRepositoryRequest struct {
	WorkspaceID string
	ContentID   string
	SourcePath  string
	MaxFiles    int
	MaxBytes    int64
}

type ProtectImportedGitVersionRequest struct {
	WorkspaceID string
	ContentID   string
	VersionID   string
	CommitSHA   string
	Limits      Limits
}

// GitTreeMigrationBackend is the narrow maintenance-window boundary for
// reusing self-contained AGS bare repositories and pinning product-retained
// commits. It is never injected into request handlers or product services.
type GitTreeMigrationBackend interface {
	ImportRepository(context.Context, ImportGitRepositoryRequest) error
	ProtectImportedVersion(context.Context, ProtectImportedGitVersionRequest) (StagedTree, error)
	ProtectImportedVersions(context.Context, []ProtectImportedGitVersionRequest, int) ([]StagedTree, error)
	VerifyRepository(context.Context, string, string) error
}

func (s *gitTreeStorage) ImportRepository(ctx context.Context, request ImportGitRepositoryRequest) error {
	ctx, cancel := withDefaultGitMigrationTimeout(ctx, defaultGitImportTimeout)
	defer cancel()
	workspaceID, err := safeID("workspace", request.WorkspaceID)
	if err != nil {
		return err
	}
	contentID, err := safeID("content", request.ContentID)
	if err != nil {
		return err
	}
	source, err := filepath.Abs(strings.TrimSpace(request.SourcePath))
	if err != nil || strings.TrimSpace(request.SourcePath) == "" {
		return fmt.Errorf("content git migration source is invalid")
	}
	maxFiles := request.MaxFiles
	if maxFiles <= 0 {
		maxFiles = DefaultGitMigrationMaxFiles
	}
	maxBytes := request.MaxBytes
	if maxBytes <= 0 {
		maxBytes = DefaultGitMigrationMaxBytes
	}
	if err := inspectGitMigrationSource(ctx, source, maxFiles, maxBytes); err != nil {
		return err
	}
	if err := validateRepositoryRefs(source, true); err != nil {
		return err
	}

	unlock, err := s.lock(ctx, workspaceID, contentID)
	if err != nil {
		return err
	}
	defer unlock()
	target := filepath.Join(s.root, workspaceID, contentID+".git")
	if _, err := os.Lstat(target); err == nil {
		return ErrAlreadyExists
	} else if !errors.Is(err, os.ErrNotExist) {
		return err
	}
	if err := os.MkdirAll(filepath.Dir(target), 0o750); err != nil {
		return err
	}
	workspace, err := openDirectoryAt(s.rootFile, []string{workspaceID}, false)
	if err != nil {
		return fmt.Errorf("open content git import workspace: %w", err)
	}
	defer workspace.Close()
	temporary, err := os.MkdirTemp(filepath.Dir(target), ".content-git-import-")
	if err != nil {
		return err
	}
	if err := os.Remove(temporary); err != nil {
		return err
	}
	published := false
	defer func() {
		if !published {
			_ = os.RemoveAll(temporary)
		}
	}()
	clone := exec.CommandContext(ctx, "git", "clone", "--mirror", "--no-hardlinks", "--", source, temporary)
	clone.Env = isolatedGitEnvironment()
	if output, err := clone.CombinedOutput(); err != nil {
		return fmt.Errorf("clone AGS bare repository for content migration: %w: %s", err, boundedCommandOutput(output))
	}
	if err := sanitizeImportedRepository(ctx, temporary); err != nil {
		return err
	}
	if err := removeImportedMutableRefs(temporary); err != nil {
		return err
	}
	if err := validateRepositoryConfig(temporary); err != nil {
		return err
	}
	if err := validateRepositoryRefs(temporary, true); err != nil {
		return err
	}
	if err := verifyBareRepository(ctx, temporary); err != nil {
		return err
	}
	if err := os.MkdirAll(filepath.Join(temporary, "refs", "content-versions"), 0o750); err != nil {
		return err
	}
	if err := s.syncImportedRepository(ctx, temporary); err != nil {
		return err
	}
	temporaryName := filepath.Base(temporary)
	targetName := filepath.Base(target)
	if err := renameAt(workspace, temporaryName, workspace, targetName); err != nil {
		return fmt.Errorf("publish imported content repository: %w", err)
	}
	rollback := func(primary error) error {
		if rollbackErr := s.rollbackImportedRepository(workspace, targetName, temporaryName); rollbackErr != nil {
			return errors.Join(primary, rollbackErr)
		}
		return primary
	}
	if err := s.syncFile(workspace, "import-workspace"); err != nil {
		return rollback(fmt.Errorf("sync imported content repository parent: %w", err))
	}
	if err := s.syncFile(s.rootFile, "import-root"); err != nil {
		return rollback(fmt.Errorf("sync imported content repository root: %w", err))
	}
	opened, err := s.openRepositoryRoot(workspaceID, contentID, false)
	if err != nil {
		return rollback(err)
	}
	if err := s.markRepositoryReady(opened); err != nil {
		opened.Close()
		return rollback(err)
	}
	opened.Close()
	published = true
	return nil
}

func (s *gitTreeStorage) rollbackImportedRepository(workspace *os.File, targetName, temporaryName string) error {
	if err := renameAt(workspace, targetName, workspace, temporaryName); err != nil {
		return fmt.Errorf("rollback imported content repository: %w", err)
	}
	var syncErrors []error
	if err := s.syncFile(workspace, "import-rollback-workspace"); err != nil {
		syncErrors = append(syncErrors, fmt.Errorf("sync imported content repository rollback workspace: %w", err))
	}
	if err := s.syncFile(s.rootFile, "import-rollback-root"); err != nil {
		syncErrors = append(syncErrors, fmt.Errorf("sync imported content repository rollback root: %w", err))
	}
	return errors.Join(syncErrors...)
}

func inspectGitMigrationSource(ctx context.Context, source string, maxFiles int, maxBytes int64) error {
	info, err := os.Lstat(source)
	if err != nil {
		return fmt.Errorf("inspect content git migration source: %w", err)
	}
	if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
		return fmt.Errorf("content git migration source must be a directory")
	}
	for _, forbidden := range []string{"shallow", "commondir", "config.worktree", filepath.Join("objects", "info", "alternates"), filepath.Join("objects", "info", "http-alternates"), filepath.Join("info", "grafts"), filepath.Join("refs", "replace")} {
		if _, err := os.Lstat(filepath.Join(source, forbidden)); err == nil {
			return fmt.Errorf("content git migration source is not self-contained: %s", forbidden)
		} else if !errors.Is(err, os.ErrNotExist) {
			return err
		}
	}
	var files int
	var bytes int64
	return filepath.WalkDir(source, func(path string, entry fs.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if err := ctx.Err(); err != nil {
			return err
		}
		info, err := entry.Info()
		if err != nil {
			return err
		}
		if info.Mode()&os.ModeSymlink != 0 || (!info.Mode().IsRegular() && !info.IsDir()) {
			return fmt.Errorf("content git migration source contains special entry")
		}
		if !info.Mode().IsRegular() {
			return nil
		}
		files++
		if files > maxFiles || info.Size() < 0 || bytes > maxBytes-info.Size() {
			return fmt.Errorf("content git migration source exceeds bounds")
		}
		bytes += info.Size()
		return nil
	})
}

func sanitizeImportedRepository(ctx context.Context, repoPath string) error {
	if err := ctx.Err(); err != nil {
		return err
	}
	_ = os.RemoveAll(filepath.Join(repoPath, "hooks"))
	if err := os.Mkdir(filepath.Join(repoPath, "hooks"), 0o750); err != nil {
		return err
	}
	// Do not invoke `git config` against untrusted imported local config: an
	// include could redirect that command outside the copied repository. Replace
	// it with the complete minimal configuration accepted by the runtime.
	const config = "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = true\n"
	return os.WriteFile(filepath.Join(repoPath, "config"), []byte(config), 0o640)
}

func (s *gitTreeStorage) ProtectImportedVersion(ctx context.Context, request ProtectImportedGitVersionRequest) (StagedTree, error) {
	ctx, cancel := withDefaultGitMigrationTimeout(ctx, defaultGitProtectTimeout)
	defer cancel()
	return s.protectImportedVersion(ctx, request)
}

func (s *gitTreeStorage) protectImportedVersion(ctx context.Context, request ProtectImportedGitVersionRequest) (StagedTree, error) {
	workspaceID, err := safeID("workspace", request.WorkspaceID)
	if err != nil {
		return StagedTree{}, err
	}
	contentID, err := safeID("content", request.ContentID)
	if err != nil {
		return StagedTree{}, err
	}
	versionID, err := safeID("version", request.VersionID)
	if err != nil {
		return StagedTree{}, err
	}
	commitText := strings.ToLower(strings.TrimSpace(request.CommitSHA))
	if len(commitText) != 40 {
		return StagedTree{}, fmt.Errorf("imported content commit is invalid")
	}
	commitHash := plumbing.NewHash(commitText)
	if commitHash.IsZero() || commitHash.String() != commitText {
		return StagedTree{}, fmt.Errorf("imported content commit is invalid")
	}
	unlock, err := s.lock(ctx, workspaceID, contentID)
	if err != nil {
		return StagedTree{}, err
	}
	defer unlock()
	repo, err := s.openExisting(workspaceID, contentID)
	if err != nil {
		return StagedTree{}, err
	}
	defer repo.close()
	if _, err := repo.CommitObject(commitHash); err != nil {
		return StagedTree{}, fmt.Errorf("open imported content commit: %w", err)
	}
	limits, err := NormalizeLimits(request.Limits)
	if err != nil {
		return StagedTree{}, err
	}
	manifest, err := s.manifestForCommit(ctx, repo.Repository, workspaceID, contentID, versionID, commitHash, limits)
	if err != nil {
		return StagedTree{}, err
	}
	if err := s.syncCommitObjects(ctx, repo.Repository, repo.root, commitHash); err != nil {
		return StagedTree{}, err
	}
	refName := plumbing.ReferenceName("refs/content-versions/" + versionID)
	created := false
	if existing, err := repo.Reference(refName, true); err == nil {
		if existing.Hash() != commitHash {
			return StagedTree{}, ErrAlreadyExists
		}
	} else if !errors.Is(err, plumbing.ErrReferenceNotFound) {
		return StagedTree{}, err
	} else if created, err = s.publishImmutableRef(ctx, repo, versionID, commitHash); err != nil {
		return StagedTree{}, err
	} else if !created {
		return StagedTree{}, ErrAlreadyExists
	}
	return stagedFromManifest(gitPayloadRef(workspaceID, contentID, versionID, commitHash), manifest, created), nil
}

// ProtectImportedVersions is the bounded offline orchestration boundary. It
// returns results in request order; on failure, earlier protected versions are
// intentionally retained so the idempotent migration ledger can resume.
func (s *gitTreeStorage) ProtectImportedVersions(ctx context.Context, requests []ProtectImportedGitVersionRequest, concurrency int) ([]StagedTree, error) {
	if len(requests) == 0 || len(requests) > MaxGitMigrationProtectBatch {
		return nil, fmt.Errorf("imported content version batch must contain 1..%d versions", MaxGitMigrationProtectBatch)
	}
	if concurrency <= 0 || concurrency > MaxGitMigrationConcurrency {
		return nil, fmt.Errorf("imported content version concurrency must be 1..%d", MaxGitMigrationConcurrency)
	}
	ctx, cancel := withDefaultGitMigrationTimeout(ctx, defaultGitProtectTimeout)
	defer cancel()
	results := make([]StagedTree, len(requests))
	jobs := make(chan int)
	errCh := make(chan error, 1)
	var workers sync.WaitGroup
	for worker := 0; worker < concurrency; worker++ {
		workers.Add(1)
		logging.Go(slog.Default(), func() {
			defer workers.Done()
			for index := range jobs {
				staged, err := s.protectImportedVersion(ctx, requests[index])
				if err != nil {
					select {
					case errCh <- fmt.Errorf("protect imported content version %d: %w", index, err):
						cancel()
					default:
					}
					return
				}
				results[index] = staged
			}
		})
	}
	for index := range requests {
		select {
		case jobs <- index:
		case <-ctx.Done():
			break
		}
		if ctx.Err() != nil {
			break
		}
	}
	close(jobs)
	workers.Wait()
	select {
	case err := <-errCh:
		return nil, err
	default:
	}
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	return results, nil
}

func (s *gitTreeStorage) VerifyRepository(ctx context.Context, workspaceID, contentID string) error {
	ctx, cancel := withDefaultGitMigrationTimeout(ctx, defaultGitVerifyTimeout)
	defer cancel()
	workspaceID, err := safeID("workspace", workspaceID)
	if err != nil {
		return err
	}
	contentID, err = safeID("content", contentID)
	if err != nil {
		return err
	}
	repoPath := filepath.Join(s.root, workspaceID, contentID+".git")
	if err := validateRepositoryPath(s.root, workspaceID, repoPath); err != nil {
		return err
	}
	opened, err := s.openRepositoryRoot(workspaceID, contentID, false)
	if err != nil {
		return err
	}
	opened.Close()
	return verifyBareRepository(ctx, repoPath)
}

func verifyBareRepository(ctx context.Context, repoPath string) error {
	command := exec.CommandContext(ctx, "git", "--git-dir", repoPath, "fsck", "--full", "--strict")
	command.Env = isolatedGitEnvironment()
	if output, err := command.CombinedOutput(); err != nil {
		return fmt.Errorf("verify content bare repository: %w: %s", err, boundedCommandOutput(output))
	}
	return nil
}

func withDefaultGitMigrationTimeout(ctx context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
	if _, ok := ctx.Deadline(); ok {
		return context.WithCancel(ctx)
	}
	return context.WithTimeout(ctx, timeout)
}

func isolatedGitEnvironment() []string {
	environment := make([]string, 0, len(os.Environ())+5)
	for _, value := range os.Environ() {
		key, _, _ := strings.Cut(value, "=")
		if strings.HasPrefix(strings.ToUpper(key), "GIT_") {
			continue
		}
		environment = append(environment, value)
	}
	return append(environment,
		"GIT_CONFIG_NOSYSTEM=1",
		"GIT_CONFIG_SYSTEM="+os.DevNull,
		"GIT_CONFIG_GLOBAL="+os.DevNull,
		"GIT_CONFIG_COUNT=0",
		"GIT_TERMINAL_PROMPT=0",
	)
}

func boundedCommandOutput(output []byte) string {
	const limit = 4 << 10
	if len(output) > limit {
		output = output[:limit]
	}
	return strings.TrimSpace(string(output))
}
