package skills

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"

	git "github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/filemode"
	"github.com/go-git/go-git/v5/plumbing/object"

	"github.com/tidbcloud/AgentTeam/internal/agsbridge"
	"github.com/tidbcloud/AgentTeam/internal/contentpayload"
	"github.com/tidbcloud/AgentTeam/internal/service/content/migration"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
	reposstore "github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
)

type skillRepositoryMigrationExporter interface {
	ExportExactSkillRepository(context.Context, repos.RepositoryIdentity, string, contentpayload.Limits) (migration.Export, error)
}

type skillContentMigrationWorker interface {
	RunExact(context.Context, migration.Request, migration.Exporter, migration.Projector) (migration.Result, error)
}

type SkillContentMigrationRequest struct {
	RepositoryID             string
	CommitSHA                string
	ExpectedContentVersionID string
}

type SkillContentMigrationResult struct {
	ContentID        string
	ContentVersionID string
	RootDigest       string
	FileCount        int
	SizeBytes        int64
	SkillCount       int
	Replayed         bool
}

// RunExactSkillContentMigration exports one caller-selected primary AGS
// commit into the exact Skill lineage. It records a verified mapping and
// product projection but deliberately does not transition authority.
func (s *Service) RunExactSkillContentMigration(ctx context.Context, workspaceID, subject, skillID string, request SkillContentMigrationRequest) (output SkillContentMigrationResult, err error) {
	if s == nil || s.migrationWorker == nil || s.nativeContent == nil || !s.nativeContentEnabled {
		return SkillContentMigrationResult{}, invalidArgument("skill content migration worker is not configured")
	}
	exporter, ok := s.repositoryContent.(skillRepositoryMigrationExporter)
	if !ok {
		return SkillContentMigrationResult{}, invalidArgument("skill repository migration exporter is not configured")
	}
	skill, err := s.GetSkill(ctx, workspaceID, subject, skillID)
	if err != nil {
		return SkillContentMigrationResult{}, err
	}
	if !skillRoleCanManage(skill.Role) {
		return SkillContentMigrationResult{}, forbidden("skill requires manage access")
	}
	writeRoute, err := s.beginSkillWrite(ctx, skill.WorkspaceID, skill.ID)
	if err != nil {
		return SkillContentMigrationResult{}, err
	}
	if writeRoute.native {
		return SkillContentMigrationResult{}, invalidArgument("skill content migration requires legacy read ownership")
	}
	skill, err = s.GetSkill(ctx, workspaceID, subject, skillID)
	if err != nil {
		return SkillContentMigrationResult{}, err
	}
	expectedVersionID := strings.TrimSpace(request.ExpectedContentVersionID)
	repo, err := s.repositoryStore.Get(ctx, skill.WorkspaceID, reposstore.ResourceTypeSkillRepository, skill.ID, reposstore.RepoRolePrimary)
	if err != nil {
		return SkillContentMigrationResult{}, mapStoreError(err)
	}
	if strings.TrimSpace(repo.AGSRepoID) == "" || strings.TrimSpace(repo.AGSRepoID) != strings.TrimSpace(request.RepositoryID) {
		return SkillContentMigrationResult{}, invalidArgument("skill migration repository identity does not match primary mapping")
	}
	commitSHA := strings.ToLower(strings.TrimSpace(request.CommitSHA))
	limits := contentpayload.Limits{
		MaxFiles: migration.MigrationMaxFiles, MaxFileBytes: skillContentMaxFileBytes,
		MaxTotalBytes: skillContentMaxTotalBytes, MaxManifestBytes: skillContentMaxManifestBytes,
	}
	var skillCount int
	result, err := s.migrationWorker.RunExact(ctx, migration.Request{
		WorkspaceID: skill.WorkspaceID, BindingKey: skill.ID, SourceCursor: commitSHA,
		RepositoryID: repo.AGSRepoID, CommitSHA: commitSHA, ExpectedHeadVersionID: expectedVersionID,
		Actor: strings.TrimSpace(subject), Limits: limits,
	}, func(exportCtx context.Context, exportLimits contentpayload.Limits) (migration.Export, error) {
		return exporter.ExportExactSkillRepository(exportCtx, repos.IdentityFromStore(repo), commitSHA, exportLimits)
	}, func(projectCtx context.Context, migration migration.Result) error {
		preview, err := s.nativeContent.List(projectCtx, nativeSkillRequest(skill.ID, skill.WorkspaceID, migration.ContentID, migration.VersionID, subject))
		if err != nil {
			return err
		}
		if preview.Truncated {
			return invalidArgument("migrated skill repository exceeds scan limit")
		}
		skillCount = len(validDetectedSkills(preview.DetectedSkills))
		_, _, err = s.finalizeNativeSkillVersion(projectCtx, skill, strings.TrimSpace(subject), NativeSkillVersionResult{
			ContentID: migration.ContentID, VersionID: migration.VersionID, RootDigest: migration.RootDigest,
			FileCount: migration.FileCount, SizeBytes: migration.SizeBytes, SkillCount: skillCount, Preview: preview,
		}, "", "migration", migration.JobID, nil, "")
		return err
	})
	if err != nil {
		return SkillContentMigrationResult{}, err
	}
	if skillCount == 0 {
		if version, found, loadErr := s.store.FindSkillVersionByContentVersion(ctx, skill.WorkspaceID, skill.ID, result.ContentID, result.VersionID); loadErr != nil {
			return SkillContentMigrationResult{}, mapStoreError(loadErr)
		} else if found {
			skillCount = version.SkillCount
		}
	}
	return SkillContentMigrationResult{
		ContentID: result.ContentID, ContentVersionID: result.VersionID, RootDigest: result.RootDigest,
		FileCount: result.FileCount, SizeBytes: result.SizeBytes, SkillCount: skillCount, Replayed: result.Replayed,
	}, nil
}

// ExportExactSkillRepository clones only the primary branch selected by the
// product mapping, verifies its full commit SHA, and reads one bounded tree.
func (i GoGitRepositoryContentManager) ExportExactSkillRepository(ctx context.Context, repository repos.RepositoryIdentity, expectedCommitSHA string, limits contentpayload.Limits) (migration.Export, error) {
	expectedCommitSHA = strings.ToLower(strings.TrimSpace(expectedCommitSHA))
	if !skillCommitSHAPattern.MatchString(expectedCommitSHA) {
		return migration.Export{}, invalidArgument("skill migration requires a full commit SHA")
	}
	if err := i.requireExactSkillMigrationRepository(ctx, repository); err != nil {
		return migration.Export{}, err
	}
	workDir, cleanup, err := i.tempDir("meshx-skill-migration-*")
	if err != nil {
		return migration.Export{}, err
	}
	defer cleanup()
	repo, err := i.cloneExactSkillMigrationRepository(ctx, repository, workDir)
	if err != nil {
		return migration.Export{}, err
	}
	head, err := repo.Head()
	if err != nil {
		return migration.Export{}, err
	}
	if head.Hash().String() != expectedCommitSHA {
		return migration.Export{}, ErrSkillRepositorySnapshotChanged
	}
	commit, err := repo.CommitObject(head.Hash())
	if err != nil {
		return migration.Export{}, err
	}
	tree, err := commit.Tree()
	if err != nil {
		return migration.Export{}, err
	}
	files, err := exportExactSkillTree(ctx, tree, limits)
	if err != nil {
		return migration.Export{}, err
	}
	if len(files) == 0 {
		return migration.Export{}, invalidArgument("skill migration source is empty")
	}
	return migration.Export{Files: files}, nil
}

func (i GoGitRepositoryContentManager) cloneExactSkillMigrationRepository(ctx context.Context, repository repos.RepositoryIdentity, target string) (*git.Repository, error) {
	remoteURL, err := i.remoteURL(repository)
	if err != nil {
		return nil, err
	}
	options := skillMigrationCloneOptions(remoteURL, repository.DefaultBranch)
	if auth := i.targetAuth(); auth != nil {
		options.Auth = auth
	}
	repo, err := git.PlainCloneContext(ctx, target, true, options)
	if err != nil {
		return nil, mapSkillGitError(err)
	}
	return repo, nil
}

func skillMigrationCloneOptions(remoteURL, defaultBranch string) *git.CloneOptions {
	options := &git.CloneOptions{URL: remoteURL, SingleBranch: true, Depth: 1, Tags: git.NoTags}
	if branch := strings.TrimSpace(defaultBranch); branch != "" {
		options.ReferenceName = plumbing.NewBranchReferenceName(branch)
	}
	return options
}

func exportExactSkillTree(ctx context.Context, root *object.Tree, limits contentpayload.Limits) ([]migration.File, error) {
	type pendingTree struct {
		path string
		tree *object.Tree
	}
	pending := []pendingTree{{tree: root}}
	files := make([]migration.File, 0)
	var total int64
	for len(pending) > 0 {
		current := pending[len(pending)-1]
		pending = pending[:len(pending)-1]
		for index := len(current.tree.Entries) - 1; index >= 0; index-- {
			if err := ctx.Err(); err != nil {
				return nil, err
			}
			entry := current.tree.Entries[index]
			path := entry.Name
			if current.path != "" {
				path = current.path + "/" + entry.Name
			}
			switch entry.Mode {
			case filemode.Dir:
				child, err := current.tree.Tree(entry.Name)
				if err != nil {
					return nil, err
				}
				pending = append(pending, pendingTree{path: path, tree: child})
			case filemode.Regular, filemode.Executable:
				blob, err := current.tree.TreeEntryFile(&entry)
				if err != nil {
					return nil, err
				}
				if blob.Size < 0 || len(files) >= limits.MaxFiles || blob.Size > limits.MaxFileBytes || total > limits.MaxTotalBytes-blob.Size {
					return nil, invalidArgument("skill migration source exceeds file or byte bounds")
				}
				reader, err := blob.Reader()
				if err != nil {
					return nil, err
				}
				data, readErr := io.ReadAll(io.LimitReader(reader, blob.Size+1))
				closeErr := reader.Close()
				if readErr != nil {
					return nil, readErr
				}
				if closeErr != nil {
					return nil, closeErr
				}
				if int64(len(data)) != blob.Size {
					return nil, fmt.Errorf("skill migration source changed while reading %q", path)
				}
				total += blob.Size
				files = append(files, migration.File{Path: path, Data: data, Executable: entry.Mode == filemode.Executable})
			default:
				return nil, invalidArgument("skill migration source contains symlink, gitlink, or special entry")
			}
		}
	}
	return files, nil
}

const maxSkillMigrationRepositoryResponseBytes = int64(64 << 10)

func (i GoGitRepositoryContentManager) requireExactSkillMigrationRepository(ctx context.Context, repository repos.RepositoryIdentity) error {
	expectedID := strings.TrimSpace(repository.ID)
	owner, name, ok := strings.Cut(strings.TrimSpace(repository.FullName), "/")
	if !ok {
		owner, name = strings.TrimSpace(repository.Owner), strings.TrimSpace(repository.Name)
	}
	if expectedID == "" || strings.TrimSpace(owner) == "" || strings.TrimSpace(name) == "" {
		return invalidArgument("skill migration repository identity is required")
	}
	path := "/api/v3/repos/" + url.PathEscape(strings.TrimSpace(owner)) + "/" + url.PathEscape(strings.TrimSpace(name))
	status, body, err := i.readSkillMigrationRepository(ctx, path)
	if err != nil {
		return fmt.Errorf("inspect skill migration repository: %w", err)
	}
	if status < 200 || status >= 300 {
		return fmt.Errorf("inspect skill migration repository: status %d", status)
	}
	var response struct {
		ID json.RawMessage `json:"id"`
	}
	if err := json.Unmarshal(body, &response); err != nil {
		return fmt.Errorf("inspect skill migration repository: decode identity: %w", err)
	}
	actualID := strings.TrimSpace(string(response.ID))
	if strings.HasPrefix(actualID, "\"") {
		if err := json.Unmarshal(response.ID, &actualID); err != nil {
			return invalidArgument("skill migration repository identity is invalid")
		}
	}
	if strings.TrimSpace(actualID) != expectedID {
		return invalidArgument("skill migration live repository identity does not match primary mapping")
	}
	return nil
}

func (i GoGitRepositoryContentManager) readSkillMigrationRepository(ctx context.Context, path string) (int, []byte, error) {
	if i.Handler != nil {
		request := httptest.NewRequest(http.MethodGet, path, nil).WithContext(agsbridge.ContextWithSiteAdmin(ctx))
		setSkillMigrationAuthHeader(request.Header, i.AuthHeader, i.AuthorizationHeader)
		recorder := newBoundedSkillMigrationResponse(maxSkillMigrationRepositoryResponseBytes)
		i.Handler.ServeHTTP(recorder, request)
		return recorder.result()
	}
	baseURL := strings.TrimRight(strings.TrimSpace(i.BaseURL), "/")
	request, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+path, nil)
	if err != nil {
		return 0, nil, err
	}
	setSkillMigrationAuthHeader(request.Header, i.AuthHeader, i.AuthorizationHeader)
	response, err := http.DefaultClient.Do(request)
	if err != nil {
		return 0, nil, err
	}
	defer response.Body.Close()
	body, err := io.ReadAll(io.LimitReader(response.Body, maxSkillMigrationRepositoryResponseBytes+1))
	if err != nil {
		return 0, nil, err
	}
	if int64(len(body)) > maxSkillMigrationRepositoryResponseBytes {
		return 0, nil, fmt.Errorf("repository response exceeds byte limit")
	}
	return response.StatusCode, body, nil
}

func setSkillMigrationAuthHeader(header http.Header, name, value string) {
	name = strings.TrimSpace(name)
	if name == "" {
		name = "Authorization"
	}
	if value = strings.TrimSpace(value); value != "" {
		header.Set(name, value)
	}
}

type boundedSkillMigrationResponse struct {
	header   http.Header
	status   int
	limit    int64
	body     strings.Builder
	exceeded bool
}

func newBoundedSkillMigrationResponse(limit int64) *boundedSkillMigrationResponse {
	return &boundedSkillMigrationResponse{header: make(http.Header), limit: limit}
}

func (response *boundedSkillMigrationResponse) Header() http.Header { return response.header }

func (response *boundedSkillMigrationResponse) WriteHeader(status int) {
	if response.status == 0 {
		response.status = status
	}
}

func (response *boundedSkillMigrationResponse) Write(data []byte) (int, error) {
	if response.status == 0 {
		response.status = http.StatusOK
	}
	remaining := response.limit + 1 - int64(response.body.Len())
	if remaining > 0 {
		keep := int64(len(data))
		if keep > remaining {
			keep = remaining
		}
		_, _ = response.body.Write(data[:keep])
	}
	if int64(response.body.Len()) > response.limit || int64(len(data)) > remaining {
		response.exceeded = true
	}
	return len(data), nil
}

func (response *boundedSkillMigrationResponse) result() (int, []byte, error) {
	if response.exceeded {
		return 0, nil, fmt.Errorf("repository response exceeds byte limit")
	}
	status := response.status
	if status == 0 {
		status = http.StatusOK
	}
	return status, []byte(response.body.String()), nil
}
