package handlers

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
)

const githubBlobMaxBytes = 100 * 1024 * 1024

func collectDirFiles(rootDir string) ([]gitPathFile, []string) {
	cleanRoot := filepath.Clean(rootDir)
	byPath := map[string]gitPathFile{}
	var warnings []string

	_ = filepath.Walk(cleanRoot, func(path string, info os.FileInfo, walkErr error) error {
		if walkErr != nil {
			warnings = append(warnings, path+": "+walkErr.Error())
			return nil
		}
		if info.IsDir() {
			if shouldSkipBundledPath(filepath.Base(path)) && path != cleanRoot {
				return filepath.SkipDir
			}
			return nil
		}
		rel, err := filepath.Rel(cleanRoot, path)
		if err != nil {
			warnings = append(warnings, path+": "+err.Error())
			return nil
		}
		rel = filepath.ToSlash(rel)
		if rel == "" || strings.HasPrefix(rel, "..") || shouldSkipBundledPath(rel) {
			return nil
		}
		if info.Size() > githubBlobMaxBytes {
			warnings = append(warnings, rel+": file quá lớn (>100MB), bỏ qua")
			return nil
		}
		data, err := os.ReadFile(path)
		if err != nil {
			warnings = append(warnings, rel+": "+err.Error())
			return nil
		}
		byPath[rel] = gitPathFile{path: rel, content: data}
		return nil
	})

	out := make([]gitPathFile, 0, len(byPath))
	for _, f := range byPath {
		out = append(out, f)
	}
	return out, warnings
}

type gitPathFile struct {
	path    string
	content []byte
}

func githubRepoAPI(owner, repo, suffix string) string {
	return fmt.Sprintf("https://api.github.com/repos/%s/%s%s",
		url.PathEscape(strings.TrimSpace(owner)),
		url.PathEscape(strings.TrimSpace(repo)),
		suffix,
	)
}

func resolveGitPushTarget(token, fallbackOwner, repoName, repoHTML string) (owner, repo string) {
	if o, r, err := parseGitHubRepo(repoHTML); err == nil && o != "" && r != "" {
		return o, r
	}
	owner = strings.TrimSpace(fallbackOwner)
	if live, err := githubGetUser(token); err == nil && live != "" {
		owner = live
	}
	return owner, trimGitHubRepoName(repoName)
}

func githubCreateBlob(token, owner, repo string, content []byte) (string, error) {
	apiURL := githubRepoAPI(owner, repo, "/git/blobs")
	body := map[string]string{
		"content":  base64.StdEncoding.EncodeToString(content),
		"encoding": "base64",
	}
	var resp struct {
		SHA string `json:"sha"`
	}
	if err := githubJSON(token, http.MethodPost, apiURL, body, &resp); err != nil {
		return "", err
	}
	if resp.SHA == "" {
		return "", errors.New("GitHub không trả blob sha")
	}
	return resp.SHA, nil
}

func githubCreateTree(token, owner, repo, baseTreeSHA string, entries []map[string]string) (string, error) {
	apiURL := githubRepoAPI(owner, repo, "/git/trees")
	body := map[string]any{"tree": entries}
	if baseTreeSHA != "" {
		body["base_tree"] = baseTreeSHA
	}
	var resp struct {
		SHA string `json:"sha"`
	}
	if err := githubJSON(token, http.MethodPost, apiURL, body, &resp); err != nil {
		return "", err
	}
	if resp.SHA == "" {
		return "", errors.New("GitHub không trả tree sha")
	}
	return resp.SHA, nil
}

func githubCreateCommit(token, owner, repo, treeSHA, parentSHA, message string) (string, error) {
	apiURL := githubRepoAPI(owner, repo, "/git/commits")
	body := map[string]any{
		"message": message,
		"tree":    treeSHA,
	}
	if parentSHA != "" {
		body["parents"] = []string{parentSHA}
	}
	var resp struct {
		SHA string `json:"sha"`
	}
	if err := githubJSON(token, http.MethodPost, apiURL, body, &resp); err != nil {
		return "", err
	}
	if resp.SHA == "" {
		return "", errors.New("GitHub không trả commit sha")
	}
	return resp.SHA, nil
}

func githubUpdateBranchRef(token, owner, repo, branch, commitSHA string) error {
	apiURL := githubRepoAPI(owner, repo, "/git/refs/heads/"+url.PathEscape(branch))
	body := map[string]any{"sha": commitSHA, "force": false}
	err := githubJSON(token, http.MethodPatch, apiURL, body, nil)
	if err == nil {
		return nil
	}
	// Branch chưa tồn tại — tạo mới
	createURL := githubRepoAPI(owner, repo, "/git/refs")
	createBody := map[string]string{
		"ref": "refs/heads/" + branch,
		"sha": commitSHA,
	}
	if createErr := githubJSON(token, http.MethodPost, createURL, createBody, nil); createErr == nil {
		return nil
	} else if !strings.Contains(strings.ToLower(createErr.Error()), "already exists") {
		return createErr
	}
	// Ref đã tồn tại nhưng PATCH thất bại (non-fast-forward) — cập nhật force
	forceBody := map[string]any{"sha": commitSHA, "force": true}
	return githubJSON(token, http.MethodPatch, apiURL, forceBody, nil)
}

func githubBranchTip(token, owner, repo, branch string) (commitSHA, treeSHA string, err error) {
	apiURL := githubRepoAPI(owner, repo, "/git/ref/heads/"+url.PathEscape(branch))
	var refResp struct {
		Object struct {
			SHA string `json:"sha"`
		} `json:"object"`
	}
	if err := githubJSON(token, http.MethodGet, apiURL, nil, &refResp); err != nil {
		return "", "", err
	}
	commitSHA = refResp.Object.SHA
	if commitSHA == "" {
		return "", "", errors.New("không lấy được commit hiện tại")
	}
	commitURL := githubRepoAPI(owner, repo, "/git/commits/"+url.PathEscape(commitSHA))
	var commitResp struct {
		Tree struct {
			SHA string `json:"sha"`
		} `json:"tree"`
	}
	if err := githubJSON(token, http.MethodGet, commitURL, nil, &commitResp); err != nil {
		return "", "", err
	}
	return commitSHA, commitResp.Tree.SHA, nil
}

func githubJSON(token, method, apiURL string, body any, out any) error {
	var reader io.Reader
	if body != nil {
		payload, err := json.Marshal(body)
		if err != nil {
			return err
		}
		reader = bytes.NewReader(payload)
	}
	req, err := http.NewRequest(method, apiURL, reader)
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Accept", "application/vnd.github+json")
	req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	raw, _ := io.ReadAll(resp.Body)
	if resp.StatusCode >= 300 {
		var errBody struct {
			Message string `json:"message"`
		}
		_ = json.Unmarshal(raw, &errBody)
		if errBody.Message != "" {
			return errors.New(errBody.Message)
		}
		return fmt.Errorf("GitHub API lỗi (%d)", resp.StatusCode)
	}
	if out != nil && len(raw) > 0 {
		if err := json.Unmarshal(raw, out); err != nil {
			return err
		}
	}
	return nil
}

// githubPushFiles — đẩy danh sách file lên repo; file lỗi được bỏ qua.
func githubPushFiles(token, owner, repo, branch string, files []gitPathFile, message string) (string, []string, error) {
	warnings := []string{}
	if len(files) == 0 {
		return "", warnings, errors.New("không có file để đẩy")
	}

	parentSHA, baseTreeSHA, tipErr := githubBranchTip(token, owner, repo, branch)
	if tipErr != nil {
		parentSHA = ""
		baseTreeSHA = ""
	}

	entries := make([]map[string]string, 0, len(files))
	for _, f := range files {
		blobSHA, err := githubCreateBlob(token, owner, repo, f.content)
		if err != nil {
			warnings = append(warnings, f.path+": "+err.Error()+" (bỏ qua)")
			continue
		}
		entries = append(entries, map[string]string{
			"path": f.path,
			"mode": "100644",
			"type": "blob",
			"sha":  blobSHA,
		})
	}

	if len(entries) == 0 {
		return "", warnings, errors.New("không đẩy được file nào lên GitHub")
	}

	treeSHA, err := githubCreateTree(token, owner, repo, baseTreeSHA, entries)
	if err != nil {
		return "", warnings, err
	}
	commitSHA, err := githubCreateCommit(token, owner, repo, treeSHA, parentSHA, message)
	if err != nil {
		return "", warnings, err
	}
	if err := githubUpdateBranchRef(token, owner, repo, branch, commitSHA); err != nil {
		return "", warnings, err
	}
	return fmt.Sprintf("https://github.com/%s/%s/tree/%s", owner, repo, branch), warnings, nil
}

// githubPushDirectory — đẩy file lên repo; file lỗi được bỏ qua, không chặn cả lần push.
func githubPushDirectory(token, owner, repo, branch, localRoot, message string) (string, []string, error) {
	files, collectWarn := collectDirFiles(localRoot)
	warnings := append([]string{}, collectWarn...)
	url, pushWarn, err := githubPushFiles(token, owner, repo, branch, files, message)
	return url, append(warnings, pushWarn...), err
}
