package canonical

import (
	"fmt"
	"sort"
	"strings"

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

// MapGitHubToAGS and MapAGSToGitHub are the only provider-direction mappers.
// Providers inventory their own native state; the mapper then classifies the
// deterministic target representation before diffing. Keeping this boundary
// explicit prevents provider writers from quietly changing diff semantics.
func MapGitHubToAGS(objects []reposync.InventoryObject) ([]reposync.InventoryObject, error) {
	return mapInventory(objects, "github", "ags")
}

func MapAGSToGitHub(objects []reposync.InventoryObject) ([]reposync.InventoryObject, error) {
	return mapInventory(objects, "ags", "github")
}

// MapForTarget applies the deployment mapper selected by the immutable
// endpoint providers. Same-provider input is useful in tests and remains a
// validated canonical identity mapping.
func MapForTarget(objects []reposync.InventoryObject, sourceProvider, targetProvider string) ([]reposync.InventoryObject, error) {
	switch sourceProvider + "->" + targetProvider {
	case "github->ags":
		return MapGitHubToAGS(objects)
	case "ags->github":
		return MapAGSToGitHub(objects)
	case "->", "ags->ags", "github->github":
		return mapInventory(objects, sourceProvider, targetProvider)
	default:
		return nil, fmt.Errorf("unsupported repository sync mapping %q -> %q", sourceProvider, targetProvider)
	}
}

func MapObjectForTarget(object reposync.InventoryObject, sourceProvider, targetProvider string) (reposync.InventoryObject, error) {
	mapped, err := MapForTarget([]reposync.InventoryObject{object}, sourceProvider, targetProvider)
	if err != nil {
		return reposync.InventoryObject{}, err
	}
	return mapped[0], nil
}

func mapInventory(objects []reposync.InventoryObject, sourceProvider, targetProvider string) ([]reposync.InventoryObject, error) {
	result := make([]reposync.InventoryObject, 0, len(objects))
	for _, object := range objects {
		sourceHash := object.SourceHash
		if sourceHash == "" {
			sourceHash = object.ContentHash
		}
		record, err := Decode(object.Payload)
		if err != nil {
			return nil, fmt.Errorf("map %s/%s: %w", object.ObjectType, object.Identity, err)
		}
		if record.Type != object.ObjectType || record.Identity != object.Identity || record.Key != object.ObjectKey {
			return nil, fmt.Errorf("map %s/%s: canonical identity does not match inventory envelope", object.ObjectType, object.Identity)
		}
		if record.SourceProvider == "" {
			record.SourceProvider = sourceProvider
		}
		if sourceProvider == "github" && targetProvider == "ags" && record.Type == TypePullRequest {
			if number := IntField(record.Fields, "_number"); number > 0 {
				headRepo := StringField(record.Fields, "_head_repo")
				baseRepo := StringField(record.Fields, "_base_repo")
				if headRepo == "" || baseRepo == "" || !strings.EqualFold(headRepo, baseRepo) {
					record.Fields["_source_head_ref"] = StringField(record.Fields, "head_ref")
					record.Fields["_source_head_repo"] = headRepo
					record.Fields["head_ref"] = fmt.Sprintf("ags-sync/pr/%d/head", number)
					record.Fields["_head_repo"] = ""
				}
			}
		}
		if sourceProvider == "ags" && targetProvider == "github" && record.Type == TypePullRequest {
			if original := StringField(record.Fields, "_source_head_ref"); original != "" {
				record.Fields["head_ref"] = original
				record.Fields["_head_repo"] = StringField(record.Fields, "_source_head_repo")
			}
		}
		if record.Type == TypeReviewRequest {
			if targetProvider == "github" {
				// GitHub reviewer identities are provider-scoped, so the safe
				// representation is a managed PR conversation note. Keep its
				// human-readable content in the comparable payload: read-back can
				// then prove that a changed request was actually projected.
				record.Fields["body"] = reviewRequestProjectionBody(record.Fields)
			} else {
				// AGS stores review requests as structured rows; a GitHub carrier's
				// display-only body must not cause a round-trip update loop.
				delete(record.Fields, "body")
			}
		}
		representation := targetRepresentation(record.Type, targetProvider, object.Representation)
		mapped, err := record.Inventory(object.Revision, representation)
		if err != nil {
			return nil, err
		}
		// The mapper never changes provider immutable identity or the source's
		// collision domain. Those are planner join inputs, not target fields.
		mapped.Identity = object.Identity
		mapped.ObjectKey = object.ObjectKey
		mapped.UniqueKey = object.UniqueKey
		mapped.ManagedLinkID = object.ManagedLinkID
		mapped.SourceHash = sourceHash
		result = append(result, mapped)
	}
	return result, nil
}

func targetRepresentation(objectType, targetProvider, current string) string {
	if objectType == TypeReaction {
		return "archive_only"
	}
	switch objectType {
	case TypeIssueComment, TypePullRequestReview, TypePullRequestReviewComment:
		return "attributed"
	case TypeReviewRequest:
		if targetProvider == "github" {
			return "projected"
		}
		return "native"
	}
	if current != "" {
		return current
	}
	return "native"
}

func reviewRequestProjectionBody(fields map[string]any) string {
	names := append(stringListField(fields["users"]), stringListField(fields["teams"])...)
	sort.Strings(names)
	if len(names) == 0 {
		return "Review request cleared."
	}
	return "Requested reviewers: " + strings.Join(names, ", ")
}

func stringListField(value any) []string {
	var result []string
	switch values := value.(type) {
	case []string:
		result = append(result, values...)
	case []any:
		for _, value := range values {
			if text, ok := value.(string); ok {
				result = append(result, text)
			}
		}
	}
	return result
}
