package reposync

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"io"
	"time"
)

var (
	ErrObjectNotFound = errors.New("sync object not found")
	ErrResultUnknown  = errors.New("sync provider result is unknown")
	ErrPermission     = errors.New("sync provider permission denied")
	ErrRateLimited    = errors.New("sync provider rate limited")
	ErrQuotaExceeded  = errors.New("sync provider quota exceeded")
)

// RateLimitError is a retryable provider pause. RetryAt is best-effort: an
// upstream may omit a reset time, in which case the worker uses a small
// conservative delay. It deliberately unwraps to ErrRateLimited so callers
// that do not need scheduling details retain the simple sentinel contract.
type RateLimitError struct {
	RetryAt time.Time
	Cause   error
}

func (e *RateLimitError) Error() string {
	if e == nil || e.Cause == nil {
		return ErrRateLimited.Error()
	}
	return e.Cause.Error()
}

func (e *RateLimitError) Unwrap() error { return ErrRateLimited }

type Endpoint struct {
	Side               string
	Provider           string
	RepositoryID       string
	RepositoryFullName string
	ConnectionID       string
	AGSRepositoryID    uint
}

// GitHubInstallation is a non-secret GitHub App connection that can be shown
// in the repository-sync setup UI. ConnectionID is the only value persisted on
// a Binding; installation tokens remain inside the provider runtime.
type GitHubInstallation struct {
	ConnectionID        string
	InstallationID      int64
	AccountLogin        string
	AccountType         string
	AccountAvatarURL    string
	ManagementURL       string
	RepositorySelection string
	Permissions         map[string]string
	PermissionsValid    bool
	MissingPermissions  []string
}

// GitHubUserIdentity is the GitHub account that completed the browser
// authorization. It is persisted only as non-secret identity metadata.
type GitHubUserIdentity struct {
	ID        int64
	Login     string
	AvatarURL string
}

// GitHubAuthorization binds the authorizing GitHub identity to exactly the
// App installations visible during that authorization. The short-lived user
// token used to produce this result is never returned or persisted.
type GitHubAuthorization struct {
	User          GitHubUserIdentity
	Installations []GitHubInstallation
}

// GitHubRepositoryOption is the immutable repository identity exposed by one
// GitHub App installation. It is deliberately small and contains no token.
type GitHubRepositoryOption struct {
	ID           string
	FullName     string
	Private      bool
	Visibility   string
	Archived     bool
	ConnectionID string
	AccountLogin string
	AccountType  string
}

// GitHubConnectionDiscoverer provides the safe, read-only picker data used by
// the web UI. Provider factories may implement it in addition to opening run
// scoped repository endpoints.
type GitHubConnectionDiscoverer interface {
	ListGitHubInstallations(context.Context) ([]GitHubInstallation, error)
	ListGitHubRepositories(context.Context, string) ([]GitHubRepositoryOption, error)
}

// GitHubConnectionAuthorizer owns the browser-based installation handshake.
// Implementations must verify the callback against the GitHub user who
// authorized the app rather than trusting installation_id alone.
type GitHubConnectionAuthorizer interface {
	GitHubInstallURL(string) (string, error)
	VerifyGitHubInstallation(context.Context, string, int64) (GitHubAuthorization, error)
	VerifyGitHubInstallationChange(context.Context, string, int64, []int64) (GitHubAuthorization, error)
}

// GitHubConnectionReauthorizer connects an AGS user to a verified GitHub
// identity and refreshes the App installations visible to that identity. The
// short-lived GitHub user token is never persisted.
type GitHubConnectionReauthorizer interface {
	GitHubAuthorizeURL(string) (string, error)
	VerifyGitHubAuthorizations(context.Context, string) (GitHubAuthorization, error)
}

// Provider is one run-scoped repository endpoint. Inventory must be complete:
// returning a partial inventory as success could incorrectly plan deletions.
// ApplyBatch may use a native bulk endpoint or bounded parallel calls, but it
// must return one result per logical RunItem whose outcome is known.
type Provider interface {
	Endpoint() Endpoint
	Inventory(context.Context) ([]InventoryObject, string, error)
	Read(context.Context, string, string) (InventoryObject, error)
	Recover(context.Context, PlannedItem) (TargetState, error)
	ApplyBatch(context.Context, PhysicalBatch, Provider) ([]ApplyResult, error)
	Verify(context.Context, PlannedItem, ApplyResult) (TargetState, error)
	Close() error
}

// EmptyChecker is the optional lightweight gate for Bulk Bootstrap. It may
// stop as soon as any durable business object is found, but must never report
// true from a partial or failed check.
type EmptyChecker interface {
	IsEmpty(context.Context) (bool, error)
}

// InventoryIndexer returns the complete authority identity/revision/hash
// index without requiring canonical payload bodies. Adapters may initially
// derive it from Inventory, but the interface lets high-volume providers use
// list/projection queries while the worker fetches details only for candidates.
type InventoryIndexer interface {
	InventoryIndex(context.Context) ([]InventoryObject, string, error)
}

// ObjectLinkAware lets an adapter validate provider-visible ownership markers
// against durable ObjectLinks before removing managed provenance from a body.
// sourceSide selects which identity in each endpoint-oriented LinkView belongs
// to this provider instance.
type ObjectLinkAware interface {
	SetObjectLinks([]LinkView, bool)
}

// VisibilityReporter exposes the repository visibility observed during the
// provider's immutable-identity validation. Workers use it to fence writes
// when the standby is more public than the authority.
type VisibilityReporter interface {
	Visibility() string
}

func EmptyInventoryDigest() string {
	sum := sha256.Sum256(nil)
	return hex.EncodeToString(sum[:])
}

// BatchReader lets a provider refresh one resource inventory once and answer
// all source revision checks for a physical batch from that same snapshot.
// The returned map is keyed by provider immutable identity. Missing entries
// are treated as objects deleted after planning.
type BatchReader interface {
	ReadBatch(context.Context, string, []string) (map[string]InventoryObject, error)
}

// BatchRecoverer performs one target refresh/read-back for a physical batch,
// while preserving RunItem as the logical recovery and audit boundary. The
// returned map is keyed by ExecutionKey(item).
type BatchRecoverer interface {
	RecoverBatch(context.Context, []PlannedItem) (map[string]TargetState, error)
}

type ProviderFactory interface {
	Open(context.Context, Endpoint) (Provider, error)
}

// BlobSource streams large durable payloads such as release assets without
// embedding binary data in RunItem JSON or physical batch requests.
type BlobSource interface {
	OpenBlob(context.Context, PlannedItem) (io.ReadCloser, int64, string, error)
}

type GitRemote struct {
	URL   string
	Token string
}

// GitRemoteSource exposes a run-scoped authenticated Git transport. Tokens are
// injected through the child process environment as Git configuration and
// must never be logged or persisted in RunItem payloads.
type GitRemoteSource interface {
	GitRemote(context.Context, bool) (GitRemote, error)
}

type ApplyResult struct {
	ObjectType     string
	ObjectKey      string
	TargetIdentity string
	TargetRevision string
	TargetHash     string
	Unknown        bool
	ErrorCode      string
	ErrorMessage   string
}

type TargetState struct {
	Exists bool
	// Managed is true only when the candidate was resolved by the persisted
	// target identity, ObjectLink marker, or another cryptographically stable
	// identity. A natural-key-only match must not be adopted on a fresh create.
	Managed  bool
	Identity string
	Revision string
	Hash     string
	Number   int
	URL      string
}

func ExecutionKey(item PlannedItem) string {
	return item.ObjectType + "\x00" + item.ObjectKey
}
