package spindle

import (
	"context"
	"errors"
	"fmt"
	pathpkg "path"
	"strings"

	"github.com/bluesky-social/indigo/atproto/syntax"

	"tangled.org/core/gitclient"
	"tangled.org/core/gitutil"
	"tangled.org/core/spindle/db"
	"tangled.org/core/spindle/knotclient"
	"tangled.org/core/workflow"
)

// no fetch here, it uses a clone that already has the commit or asks the knot
func (s *Spindle) readWorkflowDir(ctx context.Context, repoDid syntax.DID, ref string) (workflow.RawPipeline, string, error) {
	repo, err := s.db.GetRepoByDid(repoDid)
	if err != nil {
		return nil, "", fmt.Errorf("unknown repoDid %s: %w", repoDid, err)
	}
	// one client per read, so the knot is asked for its caps once
	git := s.knots.Git(knotclient.Knot{Host: repo.Knot}, repoDid)

	admitted := false
	admit := func() error {
		if admitted {
			return nil
		}
		admitted = true
		return s.workflowDefs.admit(repoDid)
	}

	commit := ref
	if !gitutil.Rev(ref).IsObjectID() {
		if err := admit(); err != nil {
			return nil, "", err
		}
		if commit, err = s.resolveWorkflowRef(ctx, git, *repo, ref); err != nil {
			return nil, "", err
		}
	}

	raw, ok, err := s.readLocalWorkflows(ctx, s.newRepoPath(repoDid), commit)
	if ok || err != nil {
		return raw, commit, err
	}
	if err := admit(); err != nil {
		return nil, "", err
	}
	raw, err = s.readKnotWorkflows(ctx, git, commit)
	return raw, commit, err
}

func (s *Spindle) resolveWorkflowRef(ctx context.Context, git *gitclient.Client, repo db.Repo, ref string) (string, error) {
	if ref == "HEAD" {
		_, hash, err := s.getRepoDefaultBranch(ctx, git, repo)
		return hash, err
	}
	hash, err := git.Resolve(ctx, ref)
	if err != nil {
		return "", fmt.Errorf("resolving %s of %s: %w", ref, repo.RepoDid, err)
	}
	return string(hash), nil
}

// no lock, a fetch replacing the clone only makes git fail, which falls back to the knot
func (s *Spindle) readLocalWorkflows(ctx context.Context, repoPath, commit string) (raw workflow.RawPipeline, ok bool, err error) {
	if _, err := gitutil.OutputOffline(ctx, "-C", repoPath, "cat-file", "-e", commit+"^{commit}"); err != nil {
		return nil, false, nil
	}
	raw, err = readWorkflowTree(ctx, gitutil.OutputOffline, repoPath, commit)
	if errors.Is(err, errTooManyWorkflowFiles) {
		return nil, true, err
	}
	return raw, err == nil, nil
}

func (s *Spindle) readKnotWorkflows(ctx context.Context, git *gitclient.Client, commit string) (workflow.RawPipeline, error) {
	files, err := git.ReadDir(ctx, gitclient.Oid(commit), workflow.WorkflowDir)
	if errors.Is(err, gitclient.ErrPathNotFound) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("listing %s: %w", workflow.WorkflowDir, err)
	}
	var raw workflow.RawPipeline
	for _, file := range files {
		if !isRegularFileMode(file.Mode) {
			continue
		}
		if len(raw) >= maxWorkflowFiles {
			return nil, fmt.Errorf("%w: more than %d files under %s", errTooManyWorkflowFiles, maxWorkflowFiles, workflow.WorkflowDir)
		}
		raw = append(raw, workflow.RawWorkflow{Name: file.Name, Contents: file.Data})
	}
	return raw, nil
}

// the knot pads modes to seven digits, git prints six
func isRegularFileMode(mode string) bool {
	mode = strings.TrimLeft(mode, "0")
	return mode == "100644" || mode == "100755"
}

type gitOutput func(ctx context.Context, args ...string) ([]byte, error)

// a symlink in .tangled could point reads at files on this host
func readWorkflowTree(ctx context.Context, git gitOutput, repoPath, commit string) (workflow.RawPipeline, error) {
	listing, err := git(ctx, "-C", repoPath, "ls-tree", "-z", commit, workflow.WorkflowDir+"/")
	if err != nil {
		return nil, fmt.Errorf("listing %s: %w", workflow.WorkflowDir, err)
	}
	var raw workflow.RawPipeline
	for _, entry := range strings.Split(string(listing), "\x00") {
		meta, path, ok := strings.Cut(entry, "\t")
		fields := strings.Fields(meta)
		if !ok || len(fields) != 3 || !isRegularFileMode(fields[0]) {
			continue
		}
		if len(raw) >= maxWorkflowFiles {
			return nil, fmt.Errorf("%w: more than %d files under %s", errTooManyWorkflowFiles, maxWorkflowFiles, workflow.WorkflowDir)
		}
		contents, err := git(ctx, "-C", repoPath, "cat-file", "blob", fields[2])
		if err != nil {
			return nil, fmt.Errorf("reading raw content of '%s': %w", path, err)
		}
		raw = append(raw, workflow.RawWorkflow{Name: pathpkg.Base(path), Contents: contents})
	}
	return raw, nil
}
