package githubmgmt

import (
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"sort"
	"strings"

	gh "github.com/google/go-github/v82/github"

	webhookdomain "github.com/codex-k8s/kodex/services/internal/control-plane/internal/domain/webhook"
)

func (c *Client) GetDefaultBranch(ctx context.Context, token string, owner string, repo string) (string, error) {
	client := c.clientWithToken(token)
	info, _, err := client.Repositories.Get(ctx, strings.TrimSpace(owner), strings.TrimSpace(repo))
	if err != nil {
		return "", fmt.Errorf("github get repository %s/%s: %w", owner, repo, err)
	}
	branch := strings.TrimSpace(info.GetDefaultBranch())
	if branch == "" {
		branch = "main"
	}
	return branch, nil
}

func (c *Client) GetFile(ctx context.Context, token string, owner string, repo string, filePath string, ref string) ([]byte, bool, error) {
	client := c.clientWithToken(token)
	filePath = strings.TrimSpace(filePath)
	if filePath == "" {
		return nil, false, fmt.Errorf("path is required")
	}
	opt := &gh.RepositoryContentGetOptions{Ref: strings.TrimSpace(ref)}
	content, _, resp, err := client.Repositories.GetContents(ctx, strings.TrimSpace(owner), strings.TrimSpace(repo), filePath, opt)
	if err != nil {
		if resp != nil && resp.StatusCode == 404 {
			return nil, false, nil
		}
		return nil, false, fmt.Errorf("github get contents %s/%s %s: %w", owner, repo, filePath, err)
	}
	if content == nil {
		return nil, false, fmt.Errorf("github path %s is not a file", filePath)
	}
	rawContent, err := decodeRepositoryContent(content)
	if err != nil {
		// Large files may come with encoding=none; fall back to download API.
		if errors.Is(err, errGitHubContentNeedsDownload) {
			rc, resp, dlErr := client.Repositories.DownloadContents(ctx, strings.TrimSpace(owner), strings.TrimSpace(repo), filePath, opt)
			if dlErr != nil {
				if resp != nil && resp.StatusCode == 404 {
					return nil, false, nil
				}
				return nil, false, fmt.Errorf("github download contents %s/%s %s: %w", owner, repo, filePath, dlErr)
			}
			if rc == nil {
				return nil, false, fmt.Errorf("github download contents %s: empty body", filePath)
			}
			body, readErr := io.ReadAll(rc)
			closeErr := rc.Close()
			if readErr != nil {
				return nil, false, fmt.Errorf("read github download %s: %w", filePath, readErr)
			}
			if closeErr != nil {
				return nil, false, fmt.Errorf("close github download %s: %w", filePath, closeErr)
			}
			if strings.TrimSpace(string(body)) == "" {
				return []byte{}, true, nil
			}
			return body, true, nil
		}
		return nil, false, fmt.Errorf("read github content %s: %w", filePath, err)
	}
	if strings.TrimSpace(rawContent) == "" {
		return []byte{}, true, nil
	}
	return []byte(rawContent), true, nil
}

func (c *Client) ListChangedFilesBetweenCommits(ctx context.Context, token string, owner string, repo string, beforeSHA string, afterSHA string) ([]string, error) {
	client := c.clientWithToken(token)
	owner = strings.TrimSpace(owner)
	repo = strings.TrimSpace(repo)
	beforeSHA = strings.TrimSpace(beforeSHA)
	afterSHA = strings.TrimSpace(afterSHA)
	if owner == "" || repo == "" {
		return nil, fmt.Errorf("owner and repository are required")
	}
	if beforeSHA == "" || afterSHA == "" {
		return nil, fmt.Errorf("before and after sha are required")
	}

	opts := &gh.ListOptions{PerPage: 250}
	changedSet := make(map[string]struct{})
	for {
		comparison, resp, err := client.Repositories.CompareCommits(ctx, owner, repo, beforeSHA, afterSHA, opts)
		if err != nil {
			return nil, fmt.Errorf("github compare commits %s...%s for %s/%s: %w", beforeSHA, afterSHA, owner, repo, err)
		}
		if comparison != nil {
			for _, file := range comparison.Files {
				if file == nil {
					continue
				}
				name := strings.TrimSpace(file.GetFilename())
				if name != "" {
					changedSet[name] = struct{}{}
				}
				previous := strings.TrimSpace(file.GetPreviousFilename())
				if previous != "" {
					changedSet[previous] = struct{}{}
				}
			}
		}
		if resp == nil || resp.NextPage == 0 {
			break
		}
		opts.Page = resp.NextPage
	}

	if len(changedSet) == 0 {
		return nil, nil
	}
	changed := make([]string, 0, len(changedSet))
	for path := range changedSet {
		changed = append(changed, path)
	}
	sort.Strings(changed)
	return changed, nil
}

func (c *Client) ResolveRefToCommitSHA(ctx context.Context, token string, owner string, repo string, ref string) (string, error) {
	client := c.clientWithToken(token)
	owner = strings.TrimSpace(owner)
	repo = strings.TrimSpace(repo)
	ref = strings.TrimSpace(ref)
	if owner == "" || repo == "" {
		return "", fmt.Errorf("owner and repository are required")
	}
	if ref == "" {
		return "", fmt.Errorf("ref is required")
	}

	normalizedRef := strings.TrimPrefix(strings.TrimPrefix(ref, "refs/heads/"), "origin/")
	normalizedRef = strings.TrimSpace(normalizedRef)
	if normalizedRef == "" {
		return "", fmt.Errorf("ref is required")
	}

	if len(normalizedRef) >= 7 && len(normalizedRef) <= 64 {
		isHex := true
		for _, ch := range strings.ToLower(normalizedRef) {
			if (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') {
				continue
			}
			isHex = false
			break
		}
		if isHex {
			return normalizedRef, nil
		}
	}

	resolved := ""
	tryResolve := func(refPath string) error {
		result, _, err := client.Git.GetRef(ctx, owner, repo, refPath)
		if err != nil {
			return err
		}
		sha := strings.TrimSpace(result.GetObject().GetSHA())
		if sha == "" {
			return fmt.Errorf("github ref %s has empty sha", refPath)
		}
		resolved = sha
		return nil
	}

	for _, candidate := range []string{
		"refs/heads/" + normalizedRef,
		normalizedRef,
	} {
		if err := tryResolve(candidate); err == nil {
			return resolved, nil
		}
	}

	branch, _, err := client.Repositories.GetBranch(ctx, owner, repo, normalizedRef, 0)
	if err != nil {
		return "", fmt.Errorf("github resolve ref %s in %s/%s: %w", normalizedRef, owner, repo, err)
	}
	sha := strings.TrimSpace(branch.GetCommit().GetSHA())
	if sha == "" {
		return "", fmt.Errorf("github branch %s has empty commit sha", normalizedRef)
	}
	return sha, nil
}

func (c *Client) GetPullRequestHead(ctx context.Context, token string, owner string, repo string, number int) (webhookdomain.GitHubPullRequestHeadDetails, error) {
	client := c.clientWithToken(token)
	owner = strings.TrimSpace(owner)
	repo = strings.TrimSpace(repo)
	if owner == "" || repo == "" {
		return webhookdomain.GitHubPullRequestHeadDetails{}, fmt.Errorf("owner and repository are required")
	}
	if number <= 0 {
		return webhookdomain.GitHubPullRequestHeadDetails{}, fmt.Errorf("pull request number must be positive")
	}

	pr, _, err := client.PullRequests.Get(ctx, owner, repo, number)
	if err != nil {
		return webhookdomain.GitHubPullRequestHeadDetails{}, fmt.Errorf("github get pull request %s/%s#%d: %w", owner, repo, number, err)
	}
	return webhookdomain.GitHubPullRequestHeadDetails{
		State:   strings.TrimSpace(pr.GetState()),
		HeadRef: strings.TrimSpace(pr.GetHead().GetRef()),
		HeadSHA: strings.TrimSpace(pr.GetHead().GetSHA()),
	}, nil
}

var errGitHubContentNeedsDownload = errors.New("github content needs download")

func decodeRepositoryContent(content *gh.RepositoryContent) (string, error) {
	if content == nil {
		return "", fmt.Errorf("content is required")
	}

	encoding := strings.TrimSpace(content.GetEncoding())
	switch encoding {
	case "base64":
		if content.Content == nil {
			return "", fmt.Errorf("malformed response: base64 encoding of null content")
		}
		// GitHub returns base64 with embedded newlines; go-github uses DecodeString which fails on them.
		cleaned := strings.NewReplacer("\n", "", "\r", "").Replace(*content.Content)
		decoded, err := base64.StdEncoding.DecodeString(cleaned)
		if err != nil {
			return "", err
		}
		return string(decoded), nil
	case "":
		if content.Content == nil {
			return "", nil
		}
		return *content.Content, nil
	case "none":
		return "", errGitHubContentNeedsDownload
	default:
		return "", fmt.Errorf("unsupported content encoding: %s", encoding)
	}
}

func (c *Client) CommitFilesOnBranch(ctx context.Context, token string, owner string, repo string, branch string, baseSHA string, message string, files map[string][]byte) (string, error) {
	client := c.clientWithToken(token)
	owner = strings.TrimSpace(owner)
	repo = strings.TrimSpace(repo)
	branch = strings.TrimSpace(branch)
	baseSHA = strings.TrimSpace(baseSHA)
	message = strings.TrimSpace(message)
	if owner == "" || repo == "" {
		return "", fmt.Errorf("owner and repository are required")
	}
	if branch == "" {
		return "", fmt.Errorf("branch is required")
	}
	if len(files) == 0 {
		return "", fmt.Errorf("files are required")
	}
	if message == "" {
		message = "chore: update files"
	}

	return c.createCommitOnBranch(ctx, client, owner, repo, branch, baseSHA, message, files, false)
}

func (c *Client) CreatePullRequestWithFiles(ctx context.Context, token string, owner string, repo string, baseBranch string, headBranch string, title string, body string, files map[string][]byte) (prNumber int, prURL string, err error) {
	client := c.clientWithToken(token)
	owner = strings.TrimSpace(owner)
	repo = strings.TrimSpace(repo)
	if owner == "" || repo == "" {
		return 0, "", fmt.Errorf("owner and repository are required")
	}
	baseBranch = strings.TrimSpace(baseBranch)
	if baseBranch == "" {
		baseBranch = "main"
	}
	headBranch = strings.TrimSpace(headBranch)
	if headBranch == "" {
		return 0, "", fmt.Errorf("head branch is required")
	}
	if len(files) == 0 {
		return 0, "", fmt.Errorf("files are required")
	}

	refPath := "refs/heads/" + baseBranch
	ref, _, err := client.Git.GetRef(ctx, owner, repo, refPath)
	if err != nil {
		return 0, "", fmt.Errorf("github get base ref %s: %w", refPath, err)
	}
	baseSHA := strings.TrimSpace(ref.GetObject().GetSHA())
	if baseSHA == "" {
		return 0, "", fmt.Errorf("github base ref %s has empty sha", refPath)
	}

	headRef := "refs/heads/" + headBranch
	if _, _, err := client.Git.GetRef(ctx, owner, repo, headRef); err != nil {
		_, _, createErr := client.Git.CreateRef(ctx, owner, repo, gh.CreateRef{Ref: headRef, SHA: baseSHA})
		if createErr != nil {
			return 0, "", fmt.Errorf("github create head ref %s: %w", headRef, createErr)
		}
	}
	commitMsg := strings.TrimSpace(title)
	if commitMsg == "" {
		commitMsg = "chore: docset sync"
	}
	if _, err := c.createCommitOnBranch(ctx, client, owner, repo, headBranch, baseSHA, commitMsg, files, true); err != nil {
		return 0, "", err
	}

	pr, _, err := client.PullRequests.Create(ctx, owner, repo, &gh.NewPullRequest{
		Title: gh.Ptr(strings.TrimSpace(title)),
		Head:  gh.Ptr(headBranch),
		Base:  gh.Ptr(baseBranch),
		Body:  gh.Ptr(body),
	})
	if err != nil {
		return 0, "", fmt.Errorf("github create pr: %w", err)
	}
	return pr.GetNumber(), strings.TrimSpace(pr.GetHTMLURL()), nil
}

func (c *Client) createCommitOnBranch(ctx context.Context, client *gh.Client, owner string, repo string, branch string, baseSHA string, message string, files map[string][]byte, forceRefUpdate bool) (string, error) {
	if client == nil {
		return "", fmt.Errorf("github client is required")
	}
	if len(files) == 0 {
		return "", fmt.Errorf("files are required")
	}

	branch = strings.TrimSpace(branch)
	if branch == "" {
		return "", fmt.Errorf("branch is required")
	}
	baseSHA = strings.TrimSpace(baseSHA)
	if baseSHA == "" {
		refPath := "refs/heads/" + branch
		ref, _, err := client.Git.GetRef(ctx, owner, repo, refPath)
		if err != nil {
			return "", fmt.Errorf("github get ref %s: %w", refPath, err)
		}
		baseSHA = strings.TrimSpace(ref.GetObject().GetSHA())
		if baseSHA == "" {
			return "", fmt.Errorf("github ref %s has empty sha", refPath)
		}
	}

	baseCommit, _, err := client.Git.GetCommit(ctx, owner, repo, baseSHA)
	if err != nil {
		return "", fmt.Errorf("github get base commit %s: %w", baseSHA, err)
	}
	baseTreeSHA := strings.TrimSpace(baseCommit.GetTree().GetSHA())
	if baseTreeSHA == "" {
		return "", fmt.Errorf("github base tree sha is empty")
	}

	paths := make([]string, 0, len(files))
	for filePath := range files {
		trimmed := strings.TrimSpace(filePath)
		if trimmed == "" {
			return "", fmt.Errorf("file path is empty")
		}
		paths = append(paths, trimmed)
	}
	sort.Strings(paths)

	entries := make([]*gh.TreeEntry, 0, len(paths))
	for _, filePath := range paths {
		data := files[filePath]
		blob, _, err := client.Git.CreateBlob(ctx, owner, repo, gh.Blob{
			Content:  gh.Ptr(string(data)),
			Encoding: gh.Ptr("utf-8"),
		})
		if err != nil {
			return "", fmt.Errorf("github create blob %s: %w", filePath, err)
		}
		entries = append(entries, &gh.TreeEntry{
			Path: gh.Ptr(filePath),
			Mode: gh.Ptr("100644"),
			Type: gh.Ptr("blob"),
			SHA:  blob.SHA,
		})
	}

	tree, _, err := client.Git.CreateTree(ctx, owner, repo, baseTreeSHA, entries)
	if err != nil {
		return "", fmt.Errorf("github create tree: %w", err)
	}
	newCommit, _, err := client.Git.CreateCommit(ctx, owner, repo, gh.Commit{
		Message: gh.Ptr(strings.TrimSpace(message)),
		Tree:    tree,
		Parents: []*gh.Commit{{SHA: gh.Ptr(baseSHA)}},
	}, nil)
	if err != nil {
		return "", fmt.Errorf("github create commit: %w", err)
	}
	newSHA := strings.TrimSpace(newCommit.GetSHA())
	if newSHA == "" {
		return "", fmt.Errorf("github created commit sha is empty")
	}

	if _, _, err := client.Git.UpdateRef(ctx, owner, repo, "heads/"+branch, gh.UpdateRef{
		SHA:   newSHA,
		Force: gh.Ptr(forceRefUpdate),
	}); err != nil {
		return "", fmt.Errorf("github update ref heads/%s: %w", branch, err)
	}
	return newSHA, nil
}
