package gitops

import (
	"context"
	"errors"
	"strings"

	"github.com/alexei/tinyforge/internal/staticsite"
)

// maxConfigBytes caps the .tinyforge.yml fetch. The file is tiny; the cap
// stops a hostile/misconfigured repo from streaming an unbounded body.
const maxConfigBytes = 64 * 1024

// Status is the outcome of a Fetch. All outcomes are values (not errors) so a
// caller always has something to show: an absent file or a provider blip is a
// normal state, not a 500.
type Status string

const (
	StatusOK          Status = "ok"           // file present and parsed
	StatusNoFile      Status = "no_file"      // GitOps enabled, no file at path
	StatusFetchFailed Status = "fetch_failed" // transport/auth/5xx error
	StatusInvalid     Status = "invalid"      // file present but failed to parse
)

// RepoRef is the minimal repo locator Fetch needs. The caller (API layer)
// extracts these from the workload's source_config and decrypts the token —
// this package stays decoupled from the store and source plugins.
type RepoRef struct {
	Provider string // "gitea" | "github" | "gitlab" | "" (autodetect from BaseURL)
	BaseURL  string
	Owner    string
	Repo     string
	Branch   string
	Token    string // decrypted; "" for public repos
	Path     string // repo-relative file path; defaults to .tinyforge.yml
}

// Result carries everything the API/UI needs about a fetch. Message is a
// human-safe, token-redacted detail for non-ok statuses.
type Result struct {
	Status    Status
	Raw       []byte
	Spec      Spec
	CommitSHA string
	Message   string
}

// Fetch reads the .tinyforge.yml from a workload's repo and parses it. Every
// failure mode is encoded in Result.Status (never a returned error), with any
// detail token-redacted in Result.Message. A missing file is StatusNoFile, not
// a failure — never a reason to block or clear config.
func Fetch(ctx context.Context, ref RepoRef) Result {
	provider, err := staticsite.NewGitProvider(staticsite.ProviderType(ref.Provider), ref.BaseURL, ref.Token)
	if err != nil {
		return Result{Status: StatusFetchFailed, Message: redact(err, ref.Token)}
	}

	// Best-effort: the SHA lets the UI show which ref the file came from. A
	// failure here doesn't sink the fetch — the file read below is what matters.
	sha, _ := provider.GetLatestCommitSHA(ctx, ref.Owner, ref.Repo, ref.Branch)

	path := ref.Path
	if path == "" {
		path = ".tinyforge.yml"
	}
	data, err := provider.DownloadFile(ctx, ref.Owner, ref.Repo, ref.Branch, path, maxConfigBytes)
	if err != nil {
		if errors.Is(err, staticsite.ErrFileNotFound) {
			return Result{Status: StatusNoFile, CommitSHA: sha}
		}
		return Result{Status: StatusFetchFailed, CommitSHA: sha, Message: redact(err, ref.Token)}
	}

	spec, err := ParseSpec(data)
	if err != nil {
		// Parse errors describe YAML structure (line/col), not the token.
		return Result{Status: StatusInvalid, Raw: data, CommitSHA: sha, Message: err.Error()}
	}
	return Result{Status: StatusOK, Raw: data, Spec: spec, CommitSHA: sha}
}

// redact strips the access token from an error message so a fetch failure can
// be surfaced or persisted without leaking the credential (mirrors the
// sanitizeError convention in the static/dockerfile sources).
func redact(err error, token string) string {
	if err == nil {
		return ""
	}
	msg := err.Error()
	if token != "" {
		msg = strings.ReplaceAll(msg, token, "[redacted]")
	}
	return msg
}
