package automation

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"unicode/utf8"

	"github.com/tidbcloud/AgentTeam/internal/service/automation/runtime"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
)

const (
	defaultAutomationTreePublishMaxFiles     = 10000
	defaultAutomationTreePublishMaxEntries   = 20000
	defaultAutomationTreePublishMaxFileBytes = int64(16 * 1024 * 1024)
	defaultAutomationTreePublishMaxBytes     = int64(256 * 1024 * 1024)
)

var errAutomationTreePublishLimitExceeded = errors.New("automation tree publish limit exceeded")

// AutomationRepositoryProvisioner ensures an AGS repository exists before
// automation-generated content is published into it.
type AutomationRepositoryProvisioner interface {
	EnsureRepository(context.Context, repos.AGSRepositoryEnsureRequest) (repos.RepositoryIdentity, error)
}

// AGSAutomationTreePublisher publishes staged files into an AGS repository tree.
type AGSAutomationTreePublisher struct {
	Store               AutomationRepositoryProvisioner
	Handler             http.Handler
	AuthHeader          string
	AuthorizationHeader string
	MaxFiles            int
	MaxEntries          int
	MaxFileBytes        int64
	MaxBytes            int64
}

type automationTreePublishFile struct {
	Path    string
	Content string
	Size    int64
}

type automationTreePublishLimits struct {
	MaxFiles     int
	MaxEntries   int
	MaxFileBytes int64
	MaxBytes     int64
}

type automationTreePublishHead struct {
	CommitSHA string
	TreeSHA   string
	Branch    string
}

func (p AGSAutomationTreePublisher) PublishRepositoryTree(ctx context.Context, req runtime.TreePublishRequest) (runtime.TreePublishResult, error) {
	if p.Store == nil {
		return runtime.TreePublishResult{}, invalidArgument("automation tree publisher repository provisioner is not configured")
	}
	if p.Handler == nil {
		return runtime.TreePublishResult{}, invalidArgument("automation tree publisher AGS handler is not configured")
	}
	owner, name, err := automationPublishRepository(req.Repository)
	if err != nil {
		return runtime.TreePublishResult{}, err
	}
	sourceRoot := strings.TrimSpace(req.SourcePath)
	if sourceRoot == "" || !filepath.IsAbs(sourceRoot) {
		return runtime.TreePublishResult{}, invalidArgument("automation tree publish source path must be absolute")
	}
	limits := p.limits(req.MaxFiles)
	files, bytesRead, err := readAutomationTreePublishFiles(sourceRoot, limits)
	if err != nil {
		return runtime.TreePublishResult{}, err
	}
	repository, err := p.Store.EnsureRepository(ctx, repos.AGSRepositoryEnsureRequest{
		WorkspaceID:   strings.TrimSpace(req.WorkspaceID),
		ResourceType:  "automation_run",
		ResourceID:    strings.TrimSpace(req.RunID),
		RepoRole:      "tree",
		Owner:         owner,
		Name:          name,
		Description:   firstNonEmptyString(req.Description, "Automation-generated repository tree"),
		Kind:          firstNonEmptyString(req.RepositoryKind, "automation_preset_tree"),
		Visibility:    firstNonEmptyString(req.Visibility, "private"),
		UserVisible:   true,
		AutoInit:      true,
		DefaultBranch: firstNonEmptyString(req.DefaultBranch, "main"),
	})
	if err != nil {
		return runtime.TreePublishResult{}, err
	}
	owner, name, err = automationPublishRepository(repository.FullName)
	if err != nil {
		return runtime.TreePublishResult{}, err
	}
	branch := firstNonEmptyString(repository.DefaultBranch, req.DefaultBranch, "main")
	head, ok, err := p.resolveHead(ctx, owner, name, branch)
	if err != nil {
		return runtime.TreePublishResult{}, err
	}
	treeSHA, err := p.createTree(ctx, owner, name, files)
	if err != nil {
		return runtime.TreePublishResult{}, err
	}
	if ok && head.TreeSHA == treeSHA {
		return runtime.TreePublishResult{Repository: repository.FullName, Files: len(files), Bytes: bytesRead, CommitSHA: head.CommitSHA}, nil
	}
	commitSHA, err := p.createCommit(ctx, owner, name, firstNonEmptyString(req.Message, "Publish automation repository tree"), treeSHA, head.CommitSHA)
	if err != nil {
		return runtime.TreePublishResult{}, err
	}
	if err := p.updateHead(ctx, owner, name, branch, commitSHA, ok); err != nil {
		return runtime.TreePublishResult{}, err
	}
	return runtime.TreePublishResult{Repository: repository.FullName, Files: len(files), Bytes: bytesRead, CommitSHA: commitSHA}, nil
}

func (p AGSAutomationTreePublisher) limits(requestMaxFiles int) automationTreePublishLimits {
	maxFiles := p.MaxFiles
	if maxFiles <= 0 {
		maxFiles = defaultAutomationTreePublishMaxFiles
	}
	if requestMaxFiles > 0 && requestMaxFiles < maxFiles {
		maxFiles = requestMaxFiles
	}
	maxEntries := p.MaxEntries
	if maxEntries <= 0 {
		maxEntries = defaultAutomationTreePublishMaxEntries
	}
	maxFileBytes := p.MaxFileBytes
	if maxFileBytes <= 0 {
		maxFileBytes = defaultAutomationTreePublishMaxFileBytes
	}
	maxBytes := p.MaxBytes
	if maxBytes <= 0 {
		maxBytes = defaultAutomationTreePublishMaxBytes
	}
	return automationTreePublishLimits{MaxFiles: maxFiles, MaxEntries: maxEntries, MaxFileBytes: maxFileBytes, MaxBytes: maxBytes}
}

func readAutomationTreePublishFiles(root string, limits automationTreePublishLimits) ([]automationTreePublishFile, int64, error) {
	var paths []string
	entries := 0
	err := filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		entries++
		if entries > limits.MaxEntries {
			return errAutomationTreePublishLimitExceeded
		}
		if entry.IsDir() {
			if path != root && entry.Name() == ".git" {
				return filepath.SkipDir
			}
			return nil
		}
		if entry.Type()&os.ModeSymlink != 0 {
			return nil
		}
		info, err := entry.Info()
		if err != nil {
			return err
		}
		if !info.Mode().IsRegular() {
			return nil
		}
		rel, err := filepath.Rel(root, path)
		if err != nil {
			return fmt.Errorf("resolve automation tree file path: %w", err)
		}
		publishPath, ok := automationTreePublishPath(rel)
		if !ok {
			return invalidArgument(fmt.Sprintf("automation tree publish path %q is not safe to publish", rel))
		}
		if info.Size() > limits.MaxFileBytes {
			return invalidArgument(fmt.Sprintf("automation tree publish file %q exceeds %d bytes", rel, limits.MaxFileBytes))
		}
		paths = append(paths, publishPath)
		if len(paths) > limits.MaxFiles {
			return errAutomationTreePublishLimitExceeded
		}
		return nil
	})
	if errors.Is(err, errAutomationTreePublishLimitExceeded) {
		return nil, 0, invalidArgument(fmt.Sprintf("automation tree publish source exceeds %d files, %d filesystem entries, or %d bytes", limits.MaxFiles, limits.MaxEntries, limits.MaxBytes))
	}
	if err != nil {
		return nil, 0, fmt.Errorf("read automation tree publish source: %w", err)
	}
	sort.Strings(paths)
	files := make([]automationTreePublishFile, 0, len(paths))
	var bytesRead int64
	for _, publishPath := range paths {
		path := filepath.Join(root, filepath.FromSlash(publishPath))
		payload, err := os.ReadFile(path)
		if err != nil {
			return nil, 0, fmt.Errorf("read automation tree file: %w", err)
		}
		if !utf8.Valid(payload) {
			return nil, 0, invalidArgument(fmt.Sprintf("automation tree publish file %q is not valid UTF-8", publishPath))
		}
		bytesRead += int64(len(payload))
		if bytesRead > limits.MaxBytes {
			return nil, 0, invalidArgument(fmt.Sprintf("automation tree publish source exceeds %d files, %d filesystem entries, or %d bytes", limits.MaxFiles, limits.MaxEntries, limits.MaxBytes))
		}
		files = append(files, automationTreePublishFile{Path: publishPath, Content: string(payload), Size: int64(len(payload))})
	}
	if len(files) == 0 {
		return nil, 0, invalidArgument("automation tree publish source contains no files")
	}
	return files, bytesRead, nil
}

func automationTreePublishPath(path string) (string, bool) {
	path = filepath.ToSlash(filepath.Clean(strings.TrimSpace(path)))
	path = strings.Trim(path, "/")
	if path == "" || path == "." {
		return "", false
	}
	parts := strings.Split(path, "/")
	for _, part := range parts {
		if part == "" || part == "." || part == ".." || part == ".git" {
			return "", false
		}
	}
	if strings.HasPrefix(parts[0], "-") {
		return "", false
	}
	return strings.Join(parts, "/"), true
}

func (p AGSAutomationTreePublisher) resolveHead(ctx context.Context, owner string, repo string, branch string) (automationTreePublishHead, bool, error) {
	branch = firstNonEmptyString(branch, "main")
	response := doAGSInternalJSONRequest(ctx, p.Handler, p.AuthHeader, p.AuthorizationHeader, http.MethodGet, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo)+"/git/refs/heads/"+pathEscapeSegments(branch), nil)
	if response.Code == http.StatusNotFound {
		return automationTreePublishHead{Branch: branch}, false, nil
	}
	if response.Code < 200 || response.Code >= 300 {
		return automationTreePublishHead{}, false, fmt.Errorf("read AGS git ref: status %d: %s", response.Code, strings.TrimSpace(response.Body.String()))
	}
	var ref struct {
		Object struct {
			SHA string `json:"sha"`
		} `json:"object"`
	}
	if err := json.Unmarshal(response.Body.Bytes(), &ref); err != nil {
		return automationTreePublishHead{}, false, fmt.Errorf("decode AGS git ref: %w", err)
	}
	commitSHA := strings.TrimSpace(ref.Object.SHA)
	if commitSHA == "" {
		return automationTreePublishHead{}, false, fmt.Errorf("read AGS git ref: missing commit sha")
	}
	commitResponse := doAGSInternalJSONRequest(ctx, p.Handler, p.AuthHeader, p.AuthorizationHeader, http.MethodGet, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo)+"/git/commits/"+url.PathEscape(commitSHA), nil)
	if commitResponse.Code < 200 || commitResponse.Code >= 300 {
		return automationTreePublishHead{}, false, fmt.Errorf("read AGS git commit: status %d: %s", commitResponse.Code, strings.TrimSpace(commitResponse.Body.String()))
	}
	var commit struct {
		SHA  string `json:"sha"`
		Tree struct {
			SHA string `json:"sha"`
		} `json:"tree"`
	}
	if err := json.Unmarshal(commitResponse.Body.Bytes(), &commit); err != nil {
		return automationTreePublishHead{}, false, fmt.Errorf("decode AGS git commit: %w", err)
	}
	treeSHA := strings.TrimSpace(commit.Tree.SHA)
	if treeSHA == "" {
		return automationTreePublishHead{}, false, fmt.Errorf("read AGS git commit: missing tree sha")
	}
	return automationTreePublishHead{CommitSHA: firstNonEmptyString(commit.SHA, commitSHA), TreeSHA: treeSHA, Branch: branch}, true, nil
}

func (p AGSAutomationTreePublisher) createTree(ctx context.Context, owner string, repo string, files []automationTreePublishFile) (string, error) {
	tree := make([]map[string]any, 0, len(files))
	for _, file := range files {
		content := file.Content
		tree = append(tree, map[string]any{
			"path":    file.Path,
			"mode":    "100644",
			"type":    "blob",
			"content": content,
		})
	}
	response := doAGSInternalJSONRequest(ctx, p.Handler, p.AuthHeader, p.AuthorizationHeader, http.MethodPost, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo)+"/git/trees", map[string]any{"tree": tree})
	if response.Code < 200 || response.Code >= 300 {
		return "", fmt.Errorf("create AGS git tree: status %d: %s", response.Code, strings.TrimSpace(response.Body.String()))
	}
	var payload struct {
		SHA string `json:"sha"`
	}
	if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil {
		return "", fmt.Errorf("decode AGS git tree: %w", err)
	}
	if strings.TrimSpace(payload.SHA) == "" {
		return "", fmt.Errorf("create AGS git tree: missing tree sha")
	}
	return strings.TrimSpace(payload.SHA), nil
}

func (p AGSAutomationTreePublisher) createCommit(ctx context.Context, owner string, repo string, message string, treeSHA string, parentSHA string) (string, error) {
	payload := map[string]any{
		"message": message,
		"tree":    treeSHA,
	}
	if parentSHA = strings.TrimSpace(parentSHA); parentSHA != "" {
		payload["parents"] = []string{parentSHA}
	}
	response := doAGSInternalJSONRequest(ctx, p.Handler, p.AuthHeader, p.AuthorizationHeader, http.MethodPost, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo)+"/git/commits", payload)
	if response.Code < 200 || response.Code >= 300 {
		return "", fmt.Errorf("create AGS git commit: status %d: %s", response.Code, strings.TrimSpace(response.Body.String()))
	}
	var commit struct {
		SHA string `json:"sha"`
	}
	if err := json.Unmarshal(response.Body.Bytes(), &commit); err != nil {
		return "", fmt.Errorf("decode AGS git commit: %w", err)
	}
	if strings.TrimSpace(commit.SHA) == "" {
		return "", fmt.Errorf("create AGS git commit: missing commit sha")
	}
	return strings.TrimSpace(commit.SHA), nil
}

func (p AGSAutomationTreePublisher) updateHead(ctx context.Context, owner string, repo string, branch string, commitSHA string, refExists bool) error {
	branch = firstNonEmptyString(branch, "main")
	if refExists {
		response := doAGSInternalJSONRequest(ctx, p.Handler, p.AuthHeader, p.AuthorizationHeader, http.MethodPatch, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo)+"/git/refs/heads/"+pathEscapeSegments(branch), map[string]any{"sha": commitSHA, "force": false})
		if response.Code != http.StatusNotFound {
			if response.Code < 200 || response.Code >= 300 {
				return fmt.Errorf("update AGS git ref: status %d: %s", response.Code, strings.TrimSpace(response.Body.String()))
			}
			return nil
		}
	}
	response := doAGSInternalJSONRequest(ctx, p.Handler, p.AuthHeader, p.AuthorizationHeader, http.MethodPost, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo)+"/git/refs", map[string]string{"ref": "refs/heads/" + branch, "sha": commitSHA})
	if response.Code < 200 || response.Code >= 300 {
		return fmt.Errorf("create AGS git ref: status %d: %s", response.Code, strings.TrimSpace(response.Body.String()))
	}
	return nil
}

func automationPublishRepository(value string) (string, string, error) {
	owner, repo, ok := strings.Cut(strings.TrimSpace(value), "/")
	if !ok || strings.Contains(repo, "/") || strings.TrimSpace(owner) == "" || strings.TrimSpace(repo) == "" {
		return "", "", invalidArgument("automation publish repository must be owner/repo")
	}
	return strings.TrimSpace(owner), strings.TrimSpace(repo), nil
}
