package canonical

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"strings"
	"time"

	reposync "github.com/pingcap/agent-git-service/internal/sync"
)

const (
	SchemaVersion = 1
	MapperVersion = "repository-sync-canonical-v1"
)

const (
	TypeGitRef                   = "git_refs"
	TypeLFSObject                = "lfs_objects"
	TypeWikiRef                  = "wiki_refs"
	TypeLabel                    = "labels"
	TypeMilestone                = "milestones"
	TypeIssue                    = "issues"
	TypeIssueComment             = "issue_comments"
	TypePullRequest              = "pull_requests"
	TypePullRequestReview        = "pull_request_reviews"
	TypePullRequestReviewComment = "pull_request_review_comments"
	TypeReviewRequest            = "review_requests"
	TypeRelease                  = "releases"
	TypeReleaseAsset             = "release_assets"
	TypeReaction                 = "reactions"
)

var OrderedObjectTypes = []string{
	TypeGitRef, TypeLFSObject, TypeWikiRef,
	TypeLabel, TypeMilestone, TypeIssue, TypeIssueComment,
	TypePullRequest, TypePullRequestReview, TypeReviewRequest, TypePullRequestReviewComment,
	TypeRelease, TypeReleaseAsset, TypeReaction,
}

// Record is the canonical durable representation shared by both providers.
// Provider identity and provenance travel in the payload but are deliberately
// excluded from ComparableHash because target actors/timestamps cannot be
// forged and must not create an endless update loop.
type Record struct {
	Schema         int            `json:"schema"`
	Type           string         `json:"type"`
	Identity       string         `json:"identity"`
	Key            string         `json:"key"`
	UniqueKey      string         `json:"unique_key,omitempty"`
	ParentKey      string         `json:"parent_key,omitempty"`
	ParentType     string         `json:"parent_type,omitempty"`
	ParentIdentity string         `json:"parent_identity,omitempty"`
	Fields         map[string]any `json:"fields"`
	OriginalActor  string         `json:"original_actor,omitempty"`
	OriginalTime   *time.Time     `json:"original_time,omitempty"`
	SourceURL      string         `json:"source_url,omitempty"`
	SourceProvider string         `json:"source_provider,omitempty"`
	ManagedLinkID  uint           `json:"managed_link_id,omitempty"`
}

func NewRecord(objectType, identity, key string, fields map[string]any) Record {
	if fields == nil {
		fields = map[string]any{}
	}
	return Record{Schema: SchemaVersion, Type: objectType, Identity: identity, Key: key, Fields: fields}
}

func (r Record) Validate() error {
	if r.Schema != SchemaVersion || strings.TrimSpace(r.Type) == "" || strings.TrimSpace(r.Identity) == "" || strings.TrimSpace(r.Key) == "" {
		return fmt.Errorf("canonical record requires schema, type, identity, and key")
	}
	if r.Fields == nil {
		return fmt.Errorf("canonical record fields are required")
	}
	return nil
}

func (r Record) Payload() ([]byte, error) {
	if err := r.Validate(); err != nil {
		return nil, err
	}
	return json.Marshal(r)
}

func Decode(payload []byte) (Record, error) {
	var record Record
	if err := json.Unmarshal(payload, &record); err != nil {
		return Record{}, err
	}
	if err := record.Validate(); err != nil {
		return Record{}, err
	}
	return record, nil
}

func (r Record) ComparableHash() (string, error) {
	fields := make(map[string]any, len(r.Fields))
	for key, value := range r.Fields {
		// Account identities are provider-scoped. Without an explicit Binding
		// account map, a matching login string is not proof of the same person.
		// Keep assignees in provenance payloads, but exclude them from automatic
		// convergence so sync never impersonates or assigns an unrelated user.
		if strings.HasPrefix(key, "_") || key == "assignees" ||
			(r.Type == TypePullRequest && (key == "head_sha" || key == "base_sha")) ||
			(r.Type == TypePullRequestReview && key == "commit_id") {
			continue
		}
		fields[key] = value
	}
	comparable := struct {
		Schema int            `json:"schema"`
		Type   string         `json:"type"`
		Fields map[string]any `json:"fields"`
	}{r.Schema, r.Type, fields}
	payload, err := json.Marshal(comparable)
	if err != nil {
		return "", err
	}
	sum := sha256.Sum256(payload)
	return hex.EncodeToString(sum[:]), nil
}

func (r Record) Inventory(revision, representation string) (reposync.InventoryObject, error) {
	payload, err := r.Payload()
	if err != nil {
		return reposync.InventoryObject{}, err
	}
	hash, err := r.ComparableHash()
	if err != nil {
		return reposync.InventoryObject{}, err
	}
	return reposync.InventoryObject{
		ObjectType: r.Type, Identity: r.Identity, ObjectKey: r.Key, UniqueKey: r.UniqueKey,
		Revision: revision, ContentHash: hash, PayloadBytes: int64(len(payload)), Payload: payload,
		Representation: representation, ManagedLinkID: r.ManagedLinkID,
	}, nil
}

// TargetStateFromInventory carries provider read-back identity plus the
// human-facing number/URL needed for durable ObjectLink auditing.
func TargetStateFromInventory(object reposync.InventoryObject, managed bool) reposync.TargetState {
	state := reposync.TargetState{
		Exists: true, Managed: managed, Identity: object.Identity,
		Revision: object.Revision, Hash: object.ContentHash,
	}
	if record, err := Decode(object.Payload); err == nil {
		state.Number = IntField(record.Fields, "_number")
		state.URL = record.SourceURL
	}
	return state
}

func StringField(fields map[string]any, key string) string {
	value, _ := fields[key].(string)
	return value
}

func BoolField(fields map[string]any, key string) bool {
	value, _ := fields[key].(bool)
	return value
}

func IntField(fields map[string]any, key string) int {
	switch value := fields[key].(type) {
	case float64:
		return int(value)
	case int:
		return value
	case int64:
		return int(value)
	case json.Number:
		parsed, _ := value.Int64()
		return int(parsed)
	default:
		return 0
	}
}
