package web

import (
	"fmt"
	"strings"

	gitmodule "github.com/aymanbagabas/git-module"
	"github.com/charmbracelet/soft-serve/git"
)

// RefType specifies the type of git reference to fetch.
type RefType string

const (
	RefTypeBranch RefType = "branch"
	RefTypeTag    RefType = "tag"
)

// RefItem represents a git reference with its associated metadata.
type RefItem struct {
	Reference *git.Reference
	Tag       *git.Tag
	Commit    *git.Commit
}

// resolveRefOrHash resolves a ref name or commit hash to a commit hash.
// Returns the hash and whether it was resolved as a ref (true) or commit hash (false).
func resolveRefOrHash(gr *git.Repository, refOrHash string) (hash string, isRef bool, err error) {
	if refOrHash == "" {
		return "", false, fmt.Errorf("empty ref or hash")
	}

	normalizedRef := refOrHash
	if !strings.HasPrefix(refOrHash, "refs/") {
		if gr.HasTag(refOrHash) {
			normalizedRef = "refs/tags/" + refOrHash
		} else {
			normalizedRef = "refs/heads/" + refOrHash
		}
	}

	hash, err = gr.ShowRefVerify(normalizedRef)
	if err == nil {
		return hash, true, nil
	}

	if _, err := gr.CatFileCommit(refOrHash); err == nil {
		return refOrHash, false, nil
	}

	return "", false, fmt.Errorf("failed to resolve %s as ref or commit", refOrHash)
}

// parseRefAndPath splits a combined ref+path string into separate ref and path components.
// It tries progressively longer prefixes as the ref name, checking if each is a valid ref or commit.
// This allows branch names with forward slashes (e.g., "feature/branch-name") to work correctly.
// Returns the ref (short name) and path. If no valid ref is found, returns the whole string as ref.
func parseRefAndPath(gr *git.Repository, refAndPath string) (ref string, path string) {
	if refAndPath == "" {
		return "", "."
	}

	parts := strings.Split(refAndPath, "/")

	for i := len(parts); i > 0; i-- {
		potentialRef := strings.Join(parts[:i], "/")
		potentialPath := "."
		if i < len(parts) {
			potentialPath = strings.Join(parts[i:], "/")
		}

		if _, _, err := resolveRefOrHash(gr, potentialRef); err == nil {
			return potentialRef, potentialPath
		}
	}

	return refAndPath, "."
}

// resolveAndBuildRef resolves a ref or hash and builds a git.Reference object.
// Returns the reference, whether it's a commit hash (vs named ref), and any error.
func resolveAndBuildRef(gr *git.Repository, refOrHash string) (*git.Reference, bool, error) {
	hash, isRef, err := resolveRefOrHash(gr, refOrHash)
	if err != nil {
		return nil, false, err
	}

	refSpec := refOrHash
	if isRef {
		if !strings.HasPrefix(refOrHash, "refs/") {
			if gr.HasTag(refOrHash) {
				refSpec = "refs/tags/" + refOrHash
			} else {
				refSpec = "refs/heads/" + refOrHash
			}
		}
	}

	return &git.Reference{
		Reference: &gitmodule.Reference{
			ID:      hash,
			Refspec: refSpec,
		},
	}, !isRef, nil
}

// FetchRefsPaginated efficiently fetches a paginated subset of refs sorted by date.
// It uses git for-each-ref to get pre-sorted refs without loading all objects upfront.
// refType specifies whether to fetch branches or tags.
// offset and limit control pagination (set limit to -1 to fetch all remaining refs).
// defaultBranch specifies which branch to pin to the top (empty string to disable pinning).
// Returns the paginated ref items and the total count of refs.
func FetchRefsPaginated(gr *git.Repository, refType RefType, offset, limit int, defaultBranch string) ([]RefItem, int, error) {
	var refPattern, sortField, format string
	var checkRefFunc func(*git.Reference) bool

	switch refType {
	case RefTypeBranch:
		refPattern = "refs/heads"
		sortField = "-committerdate"
		format = "%(refname:short)%09%(objectname)%09%(committerdate:unix)"
		checkRefFunc = (*git.Reference).IsBranch
	case RefTypeTag:
		refPattern = "refs/tags"
		sortField = "-creatordate"
		format = "%(refname:short)%09%(*objectname)%09%(objectname)%09%(*authordate:unix)%09%(authordate:unix)%09%(contents:subject)"
		checkRefFunc = (*git.Reference).IsTag
	default:
		return nil, 0, fmt.Errorf("unsupported ref type: %s", refType)
	}

	args := []string{"for-each-ref", "--sort=" + sortField, "--format=" + format, refPattern}

	cmd := git.NewCommand(args...)
	output, err := cmd.RunInDir(gr.Path)
	if err != nil {
		return nil, 0, err
	}

	lines := strings.Split(strings.TrimSpace(string(output)), "\n")
	if len(lines) == 1 && lines[0] == "" {
		return []RefItem{}, 0, nil
	}

	// Build reference map once
	refs, err := gr.References()
	if err != nil {
		return nil, 0, err
	}

	refMap := make(map[string]*git.Reference)
	for _, r := range refs {
		if checkRefFunc(r) {
			refMap[r.Name().Short()] = r
		}
	}

	// Separate default branch from others if pinning is requested
	var defaultBranchLine string
	var otherLines []string

	if refType == RefTypeBranch && defaultBranch != "" {
		for _, line := range lines {
			fields := strings.Split(line, "\t")
			if len(fields) < 1 {
				continue
			}
			refName := fields[0]
			if refName == defaultBranch {
				defaultBranchLine = line
			} else {
				otherLines = append(otherLines, line)
			}
		}
	} else {
		otherLines = lines
	}

	// Total count includes default branch if present
	totalCount := len(otherLines)
	hasDefaultBranch := defaultBranchLine != ""
	if hasDefaultBranch {
		totalCount++
	}

	items := make([]RefItem, 0)

	// Add default branch to page 1 (offset 0)
	if hasDefaultBranch && offset == 0 {
		fields := strings.Split(defaultBranchLine, "\t")
		if len(fields) >= 2 {
			refName := fields[0]
			commitID := fields[1]

			if ref := refMap[refName]; ref != nil {
				item := RefItem{Reference: ref}
				if commitID != "" {
					item.Commit, _ = gr.CatFileCommit(commitID)
				}
				items = append(items, item)
			}
		}
	}

	// Calculate pagination for non-default branches
	// On page 1, we have one less slot because default branch takes the first position
	adjustedOffset := offset
	adjustedLimit := limit

	if hasDefaultBranch {
		if offset == 0 {
			// Page 1: we already added default branch, so fetch limit-1 items
			adjustedLimit = limit - 1
		} else {
			// Page 2+: offset needs to account for default branch being removed from the list
			adjustedOffset = offset - 1
		}
	}

	if adjustedLimit <= 0 {
		return items, totalCount, nil
	}

	// Apply pagination to non-default branches
	start := adjustedOffset
	if start >= len(otherLines) {
		return items, totalCount, nil
	}

	end := len(otherLines)
	if adjustedLimit > 0 {
		end = start + adjustedLimit
		if end > len(otherLines) {
			end = len(otherLines)
		}
	}

	// Process only the paginated subset of non-default branches
	for _, line := range otherLines[start:end] {
		fields := strings.Split(line, "\t")

		var refName, commitID string

		if refType == RefTypeTag {
			if len(fields) < 6 {
				continue
			}
			refName = fields[0]
			peeledCommitID := fields[1]
			commitID = fields[2]
			if peeledCommitID != "" {
				commitID = peeledCommitID
			}
		} else {
			if len(fields) < 2 {
				continue
			}
			refName = fields[0]
			commitID = fields[1]
		}

		ref := refMap[refName]
		if ref == nil {
			continue
		}

		item := RefItem{Reference: ref}

		if refType == RefTypeTag {
			item.Tag, _ = gr.Tag(refName)
		}

		if commitID != "" {
			item.Commit, _ = gr.CatFileCommit(commitID)
		}

		items = append(items, item)
	}

	return items, totalCount, nil
}
