package spice

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"runtime"
	"slices"
	"strings"
	"sync"
	"unicode"

	"go.abhg.dev/gs/internal/forge"
	"go.abhg.dev/gs/internal/git"
	"go.abhg.dev/gs/internal/must"
	"go.abhg.dev/gs/internal/spice/state"
)

const _generatedBranchNameLimit = 32

// GenerateBranchName generates a branch name from a commit message subject.
//
// The branch name is generated by converting the subject to lowercase,
// replacing spaces with hyphens, and removing all non-alphanumeric characters.
//
// If the subject has more than 32 characters, it is truncated to 32 characters
// at word boundaries.
func GenerateBranchName(subject string) string {
	words := strings.FieldsFunc(strings.ToLower(subject), func(r rune) bool {
		return !unicode.IsLetter(r) && !unicode.IsNumber(r)
	})
	must.NotBeEmptyf(words, "subject must not be empty")

	var name strings.Builder
	for _, w := range words {
		newLen := name.Len() + len(w)
		needHyphen := name.Len() > 0
		if needHyphen {
			newLen++
		}

		if newLen > _generatedBranchNameLimit {
			break
		}

		if needHyphen {
			name.WriteByte('-')
		}
		for _, r := range w {
			name.WriteRune(unicode.ToLower(r))
		}
	}

	return name.String()
}

// LookupBranchResponse is the response to a LookupBranch request.
// It includes information about the tracked branch.
type LookupBranchResponse struct {
	// Base is the base branch configured
	// for the requested branch.
	Base string

	// BaseHash is the last known hash of the base branch.
	// This may not match the current hash of the base branch.
	BaseHash git.Hash

	// Change is information about the published change
	// associated with the branch.
	//
	// This is nil if the branch hasn't been published yet.
	Change forge.ChangeMetadata

	// UpstreamBranch is the name of the upstream branch
	// or an empty string if the branch is not tracking an upstream branch.
	UpstreamBranch string

	// Head is the commit at the head of the branch.
	Head git.Hash

	// MergedDownstack is a list of branches that were previously
	// downstack from this branch and have since been merged into trunk.
	//
	// This is used to correctly display the history of the branch.
	MergedDownstack []json.RawMessage
}

// DeletedBranchError is returned when a branch was deleted out of band.
//
// This error is used to indicate that the branch does not exist,
// but its base might.
type DeletedBranchError struct {
	Name string

	Base     string
	BaseHash git.Hash
}

func (e *DeletedBranchError) Error() string {
	return fmt.Sprintf("tracked branch %v was deleted out of band", e.Name)
}

// LookupBranch returns information about a branch tracked by gs.
//
// It returns [git.ErrNotExist] if the branch is not known to the repository,
// [state.ErrNotExist] if the branch is not tracked,
// or a [DeletedBranchError] if the branch is tracked, but was deleted out of band.
func (s *Service) LookupBranch(ctx context.Context, name string) (*LookupBranchResponse, error) {
	resp, storeErr := s.store.LookupBranch(ctx, name)
	head, gitErr := s.repo.PeelToCommit(ctx, name)

	// Handle all scenarios:
	//
	// storeErr | gitErr | Result
	// ---------|--------|-------
	// nil      | nil    | Branch exists and is tracked
	// nil      | !nil   | Branch is tracked, but was deleted out of band
	// !nil     | nil    | Branch is not tracked
	// !nil     | !nil   | Branch is not known to the repository
	if storeErr == nil && gitErr == nil {
		// Special case:
		// Branch exists and is tracked,
		// and was previously pushed to a remote,
		// but the remote branch reference has since been deleted.
		if resp.UpstreamBranch != "" {
			ok, err := s.verifyUpstreamBranchRef(ctx, name, resp.UpstreamBranch)
			if err != nil {
				s.log.Warn("Unable to verify upstream branch reference",
					"branch", name,
					"upstream", resp.UpstreamBranch,
					"error", err)
				resp.UpstreamBranch = ""
			}
			if !ok {
				// Upstream branch reference has been deleted.
				s.log.Debug("Upstream branch reference no longer valid",
					"branch", name,
					"upstream", resp.UpstreamBranch)

				resp.UpstreamBranch = ""
			}
		}

		out := &LookupBranchResponse{
			Base:            resp.Base,
			BaseHash:        resp.BaseHash,
			UpstreamBranch:  resp.UpstreamBranch,
			Head:            head,
			MergedDownstack: resp.MergedDownstack,
		}

		if resp.ChangeMetadata != nil {
			// TODO: This is ick. Service should have a Registry.
			if f, ok := s.forges.Lookup(resp.ChangeForge); !ok {
				s.log.Warn("Ignoring unknown forge requested in change metadata",
					"forge", resp.ChangeForge)
			} else {
				md, err := f.UnmarshalChangeMetadata(resp.ChangeMetadata)
				if err != nil {
					s.log.Warn("Corrupt change metadata associated with branch",
						"branch", name,
						"metadata", string(resp.ChangeMetadata),
						"error", err,
					)
				} else {
					out.Change = md
				}
			}
		}

		return out, nil
	}

	// Only one of these errors is set.
	if (storeErr != nil) != (gitErr != nil) {
		// Branch is not tracked, but exists in the repository.
		if storeErr != nil {
			return nil, fmt.Errorf("untracked branch %v: %w", name, storeErr)
		}

		if !errors.Is(gitErr, git.ErrNotExist) {
			return nil, fmt.Errorf("resolve head: %w", gitErr)
		}

		// Branch is tracked, but was deleted out of band.
		return nil, &DeletedBranchError{
			Name:     name,
			Base:     resp.Base,
			BaseHash: resp.BaseHash,
		}
	}

	// Both errors are set.
	// If the branch is not known to the repository,
	// return the git error.
	if errors.Is(gitErr, git.ErrNotExist) {
		return nil, fmt.Errorf("resolve head: %w", gitErr)
	}

	// Otherwise, something went wrong. Surface both errors.
	return nil, errors.Join(
		fmt.Errorf("untracked branch %v: %w", name, storeErr),
		fmt.Errorf("resolve head: %w", gitErr),
	)
}

// verifyUpstreamBranchRef verifies that the upstream branch reference is
// valid, and if not, it deletes that knowledge from the branch's state.
//
// That is, if $remote/$upstreamBranch does not exist,
// $branch's local state will forget about the upstream branch,
// but the branch will not be deleted.
//
// Returns true if the upstream branch reference is valid.l
func (s *Service) verifyUpstreamBranchRef(ctx context.Context, branch, upstreamBranch string) (ok bool, err error) {
	remote, err := s.store.Remote()
	if err != nil {
		return false, nil // no remote, no upstream branch
	}

	upstreamRef := remote + "/" + upstreamBranch
	if _, err := s.repo.PeelToCommit(ctx, upstreamRef); err == nil {
		return true, nil
	}

	tx := s.store.BeginBranchTx()
	var empty string
	if err := tx.Upsert(ctx, state.UpsertRequest{
		Name:           branch,
		UpstreamBranch: &empty,
	}); err != nil {
		return false, fmt.Errorf("update branch %v: %w", branch, err)
	}

	msg := fmt.Sprintf("upstream %q deleted out of band", upstreamRef)
	if err := tx.Commit(ctx, msg); err != nil {
		return false, fmt.Errorf("commit state: %w", err)
	}

	return false, nil
}

// ForgetBranch stops tracking a branch,
// updating the upstacks for it to point to its base.
func (s *Service) ForgetBranch(ctx context.Context, name string) error {
	// This does not use LookupBranch because we don't care if the branch
	// doesn't actually exist, we just want to update the upstacks.
	branch, err := s.store.LookupBranch(ctx, name)
	if err != nil {
		if errors.Is(err, state.ErrNotExist) {
			return nil
		}
		return fmt.Errorf("lookup branch: %w", err)
	}

	// Similarly, this doesn't use ListAbove
	// because we don't want the deleted branch to be removed yet.
	branchTx := s.store.BeginBranchTx()
	for candidate, err := range s.store.ListBranches(ctx) {
		if err != nil {
			return fmt.Errorf("list branches: %w", err)
		}

		if candidate == name {
			continue
		}

		info, err := s.store.LookupBranch(ctx, candidate)
		if err != nil {
			return fmt.Errorf("lookup %v: %w", candidate, err)
		}

		if info.Base != name {
			continue
		}

		if err := branchTx.Upsert(ctx, state.UpsertRequest{
			Name:     candidate,
			Base:     branch.Base,
			BaseHash: branch.BaseHash,
		}); err != nil {
			return fmt.Errorf("change base of %v to %v: %w", candidate, branch.Base, err)
		}
		s.log.Debug("Updating upstack branch to new base",
			"branch", candidate,
			"newBase", branch.Base,
		)

	}

	if err := branchTx.Delete(ctx, name); err != nil {
		return fmt.Errorf("delete branch %v: %w", name, err)
	}

	if err := branchTx.Commit(ctx, fmt.Sprintf("untrack branch %q", name)); err != nil {
		return fmt.Errorf("update state: %w", err)
	}
	s.log.Debug("Stopped tracking", "branch", name)

	return nil
}

// RenameBranch renames a branch tracked by gs.
// This handles both, renaming the branch in the repository,
// and updating the internal state to reflect the new name.
func (s *Service) RenameBranch(ctx context.Context, oldName, newName string) error {
	oldBranch, err := s.LookupBranch(ctx, oldName)
	if err != nil {
		return fmt.Errorf("lookup %v: %w", oldName, err)
	}

	// Verify new name is not already in use.
	if _, err := s.repo.PeelToCommit(ctx, newName); err == nil {
		// TODO: A force option should override this.
		return fmt.Errorf("branch %v already exists", newName)
	}

	aboves, err := s.ListAbove(ctx, oldName)
	if err != nil {
		return fmt.Errorf("list branches above %v: %w", oldName, err)
	}

	var (
		changeForge    string
		changeMetadata json.RawMessage
	)
	if md := oldBranch.Change; md != nil {
		if f, ok := s.forges.Lookup(md.ForgeID()); ok {
			changeForge = f.ID()
			changeMetadata, err = f.MarshalChangeMetadata(md)
			if err != nil {
				return fmt.Errorf("marshal change metadata: %w", err)
			}
		}
	}

	tx := s.store.BeginBranchTx()

	// Create the new branch with the same base
	// and other state as the old branch.
	if err := tx.Upsert(ctx, state.UpsertRequest{
		Name:           newName,
		Base:           oldBranch.Base,
		BaseHash:       oldBranch.BaseHash,
		ChangeForge:    changeForge,
		ChangeMetadata: changeMetadata,
		UpstreamBranch: &oldBranch.UpstreamBranch,
	}); err != nil {
		return fmt.Errorf("create branch with name %v: %w", newName, err)
	}

	// Point the branches above the old branch to the new branch.
	for _, above := range aboves {
		if err := tx.Upsert(ctx, state.UpsertRequest{
			Name: above,
			Base: newName,
		}); err != nil {
			return fmt.Errorf("update branch %v to point to %v: %w", above, newName, err)
		}
		s.log.Debug("Updating upstack branch to new name", "upstack", above)
	}

	// Delete the old branch.
	if err := tx.Delete(ctx, oldName); err != nil {
		return fmt.Errorf("delete branch %v: %w", oldName, err)
	}

	// If we get here, the change will be committed successfully.
	// We can perform the Git rename and commit.
	if err := s.repo.RenameBranch(ctx, git.RenameBranchRequest{
		OldName: oldName,
		NewName: newName,
	}); err != nil {
		return fmt.Errorf("rename branch: %w", err)
	}

	if err := tx.Commit(ctx, fmt.Sprintf("rename %q to %q", oldName, newName)); err != nil {
		return fmt.Errorf("update state: %w", err)
	}
	s.log.Debug("Renamed tracked name of branch", "old", oldName, "new", newName)

	return nil
}

// LoadBranchItem is a single branch returned by LoadBranches.
type LoadBranchItem struct {
	// Name is the name of the branch.
	Name string

	// Head is the commit at the head of the branch.
	Head git.Hash

	// Base is the name of the branch that this branch is based on.
	Base string

	// BaseHash is the last known commit hash of the base branch.
	// This may not match the current commit hash of the base branch.
	BaseHash git.Hash

	// Change is the metadata associated with the branch.
	// This is nil if the branch has not been published.
	Change forge.ChangeMetadata

	// UpstreamBranch is the name under which this branch
	// was pushed to the upstream repository.
	UpstreamBranch string

	// MergedDownstack contains information about any branches,
	// which this one was based on, that have already been merged into trunk.
	MergedDownstack []json.RawMessage
}

// LoadBranches loads all tracked branches
// and all their information as a single operation.
//
// The returned branches are sorted by name.
func (s *Service) LoadBranches(ctx context.Context) ([]LoadBranchItem, error) {
	var (
		wg sync.WaitGroup

		mu sync.Mutex

		errs  []error
		items []LoadBranchItem

		// These will be used if we encounter any branches
		// that have been deleted out of band.
		deletedBranches = make(map[string]*DeletedBranchError)
	)
	namec := make(chan string)
	for range runtime.GOMAXPROCS(0) {
		wg.Go(func() {
			for name := range namec {
				resp, err := s.LookupBranch(ctx, name)
				if err != nil {
					if delErr := new(DeletedBranchError); errors.As(err, &delErr) {
						s.log.Infof("%v: removing...", delErr)
						mu.Lock()
						deletedBranches[name] = delErr
						mu.Unlock()
						continue
					}

					mu.Lock()
					errs = append(errs, fmt.Errorf("get branch %v: %w", name, err))
					mu.Unlock()
				}

				mu.Lock()
				items = append(items, LoadBranchItem{
					Name:            name,
					Head:            resp.Head,
					Base:            resp.Base,
					BaseHash:        resp.BaseHash,
					UpstreamBranch:  resp.UpstreamBranch,
					Change:          resp.Change,
					MergedDownstack: resp.MergedDownstack,
				})
				mu.Unlock()
			}
		})
	}

	for name, err := range s.store.ListBranches(ctx) {
		if err != nil {
			return nil, fmt.Errorf("list branches: %w", err)
		}
		namec <- name
	}
	close(namec)
	wg.Wait()

	if err := errors.Join(errs...); err != nil {
		return nil, err
	}

	slices.SortFunc(items, func(a, b LoadBranchItem) int {
		return strings.Compare(a.Name, b.Name)
	})

	if len(deletedBranches) == 0 {
		return items, nil
	}

	// Some of the branches we've loaded have been deleted out of band.
	// We'll delete these from the data store.
	tx := s.store.BeginBranchTx()

	// But first, we need to point the branches above deletes branches
	// to the bases of the deleted branches, or the bases of the bases,
	// and so on until we find a base that is not deleted.
	//
	// This will also update the LoadBranchItem instances
	// to reflect these changes so we're not re-reading the state.
	for i, item := range items {
		origBase := item.Base
		base, baseHash := item.Base, item.BaseHash

		delErr, deleted := deletedBranches[base]
		for deleted {
			base, baseHash = delErr.Base, delErr.BaseHash
			delErr, deleted = deletedBranches[base]
		}

		if base != origBase {
			if err := tx.Upsert(ctx, state.UpsertRequest{
				Name:     item.Name,
				Base:     base,
				BaseHash: baseHash,
			}); err != nil {
				s.log.Warn("Could not update base of branch upstack from deleted branch",
					"branch", item.Name,
					"newBase", item.Base,
					"error", err,
				)
				continue
			}

			item.Base = base
			item.BaseHash = baseHash
			items[i] = item
		}
	}

	// At this point, the deleted branches should not have any branches above them,
	// except those that we failed to update above.
	// Delete what we can, log the rest.
	for name := range deletedBranches {
		if err := tx.Delete(ctx, name); err != nil {
			s.log.Warn("Unable to delete branch", "branch", name, "error", err)
		}
	}

	if err := tx.Commit(ctx, "clean up deleted branches"); err != nil {
		s.log.Warn("Error cleaning up after deleted branched", "error", err)
	}

	return items, nil
}

// ListAbove returns a list of branches that are immediately above the given branch.
// These are branches that have the given branch as their base.
// The slice is empty if there are no branches above the given branch.
func (s *Service) ListAbove(ctx context.Context, base string) ([]string, error) {
	graph, err := s.BranchGraph(ctx, nil)
	if err != nil {
		return nil, fmt.Errorf("get branch graph: %w", err)
	}

	return slices.Collect(graph.Aboves(base)), nil
}

// ListUpstack will list all branches that are upstack from the given branch,
// including those that are upstack from the upstack branches.
// The given branch is the first element in the returned slice.
//
// The returned slice is ordered by branch position in the upstack.
// It is guaranteed that for i < j, branch[i] is not a parent of branch[j].
func (s *Service) ListUpstack(ctx context.Context, start string) ([]string, error) {
	graph, err := s.BranchGraph(ctx, nil)
	if err != nil {
		return nil, fmt.Errorf("get branch graph: %w", err)
	}

	upstacks := slices.Collect(graph.Upstack(start))
	if len(upstacks) == 0 {
		// Empty iterator means the branch is not tracked.
		// TOOD: should we return an error here?
		upstacks = []string{start}
	}
	must.NotBeEmptyf(upstacks, "there must be at least one branch")
	must.BeEqualf(start, upstacks[0], "starting branch must be first upstack")
	return upstacks, nil
}

// FindTop returns the topmost branches in each upstack chain
// starting at the given branch.
func (s *Service) FindTop(ctx context.Context, start string) ([]string, error) {
	graph, err := s.BranchGraph(ctx, nil)
	if err != nil {
		return nil, fmt.Errorf("get branch graph: %w", err)
	}

	return slices.Collect(graph.Tops(start)), nil
}

// ListDownstack lists all branches below the given branch
// in the downstack chain, not including trunk.
//
// The given branch is the first element in the returned slice,
// and the bottom-most branch is the last element.
//
// If there are no branches downstack because we're on trunk,
// or because all branches are downstack from trunk have been deleted,
// the returned slice will be nil.
func (s *Service) ListDownstack(ctx context.Context, start string) ([]string, error) {
	graph, err := s.BranchGraph(ctx, nil)
	if err != nil {
		return nil, fmt.Errorf("get branch graph: %w", err)
	}

	downstack := slices.Collect(graph.Downstack(start))
	if len(downstack) == 0 {
		return nil, nil // no downstack branches
	}
	must.BeEqualf(start, downstack[0], "starting branch must be first in downstack")
	return downstack, nil
}

// FindBottom returns the bottom-most branch in the downstack chain
// starting at the given branch just before trunk.
//
// Returns an error if no downstack branches are found.
func (s *Service) FindBottom(ctx context.Context, start string) (string, error) {
	graph, err := s.BranchGraph(ctx, nil)
	if err != nil {
		return "", fmt.Errorf("get branch graph: %w", err)
	}

	bottom := graph.Bottom(start)
	if bottom == "" {
		return "", fmt.Errorf("no downstack branches found for %q", start)
	}
	return bottom, nil
}

// ListStack returns the full stack of branches that the given branch is in.
//
// If the start branch has multiple upstack branches,
// all of them are included in the returned slice.
// The result is ordered by branch position in the stack
// with the bottom-most branch as the first element.
func (s *Service) ListStack(ctx context.Context, start string) ([]string, error) {
	graph, err := s.BranchGraph(ctx, nil)
	if err != nil {
		return nil, fmt.Errorf("get branch graph: %w", err)
	}

	stack := slices.Collect(graph.Stack(start))
	if len(stack) == 0 {
		return []string{start}, nil
	}
	return stack, nil
}

// ListStackLinear returns the full stack of branches that the given branch is in
// but only if the stack is linear: each branch has only one upstack branch.
// If the stack is not linear, [NonLinearStackError] is returned.
//
// The returned slice is ordered by branch position in the stack
// with the bottom-most branch as the first element.
func (s *Service) ListStackLinear(ctx context.Context, start string) ([]string, error) {
	graph, err := s.BranchGraph(ctx, nil)
	if err != nil {
		return nil, fmt.Errorf("get branch graph: %w", err)
	}

	return graph.StackLinear(start)
}
