package metadata

import (
	"context"
	"fmt"

	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
)

type contractMetadataSource interface {
	GetSchemaOrError(ctx context.Context, apiName string) (*models.ObjectMetadata, error)
	GetSchemas(ctx context.Context) []*models.ObjectMetadata
	GetRelationships(ctx context.Context, objectAPIName string) []*models.Relationship
	GetChildRelationships(ctx context.Context, parentObjectAPIName string) []*models.ObjectMetadata
	KeyPrefixForObject(ctx context.Context, objectAPIName string) (string, bool)
	ResolveObjectForRecordID(ctx context.Context, recordID string) (string, bool)
	ResolveLayout(ctx context.Context, apiName string, roleAncestors []string, profileID string, recordTypeID string) *models.PageLayout

	GetActions(ctx context.Context, objectAPIName string) []*models.ActionMetadata
	GetLiveActionByID(ctx context.Context, actionID string) (*models.ActionMetadata, error)
}

type contractLayoutMetadataSource interface {
	GetAllLayouts(ctx context.Context, objectAPIName string) ([]*models.PageLayout, error)
	GetLayoutByID(ctx context.Context, layoutID string) (*models.PageLayout, error)
	GetAssignmentsForObject(ctx context.Context, objectAPIName string) ([]*models.LayoutAssignment, error)
	GetAllApprovalLayouts(ctx context.Context, objectAPIName string) ([]*models.ApprovalPageLayout, error)
	GetApprovalLayoutByID(ctx context.Context, layoutID string) (*models.ApprovalPageLayout, error)
}

// contractLayoutAuthoringSource is deliberately its own interface rather than a
// method on contractLayoutMetadataSource. That interface is discovered by a
// single comma-ok assertion shared by the five other layout reads, so enlarging it would
// make a source implementing only the older layout methods fail the assertion
// for ALL of them and report "layout metadata source unavailable" for reads
// that still work perfectly — the same all-or-nothing failure the public
// contract.LayoutAuthoringReader split avoids one layer up.
type contractLayoutAuthoringSource interface {
	GetLayoutForAuthoring(ctx context.Context, layoutID string) (*models.PageLayout, error)
}

type contractObjectMetadataCatalogSource interface {
	GetListViewsForUser(ctx context.Context, objectAPIName string, user *models.UserSession) []*models.ListView
	GetRecordTypes(ctx context.Context, objectAPIName string) ([]*models.RecordType, error)
}

// contractPicklistScopeSource is its own interface for the reason given above
// contractLayoutAuthoringSource: a source that implements only the older reads must keep passing
// their assertions, so a newly exposed capability cannot be folded into an existing one.
//
// It exists because record-type-aware presentation dialects serve scoped picklist values, and a
// dialect had no neutral way to ask for them. The resolver was already here; only the neutral read was
// missing, so a dialect either served the field's whole value set under a record-type-keyed route or
// could not serve the route at all.
type contractPicklistScopeSource interface {
	ResolvePicklistValuesForRecordType(ctx context.Context, objectAPIName string, recordTypeID string, fieldAPIName string) (*models.ScopedPicklistValueSet, error)
}

// roleAncestorsFunc resolves the ancestor role chain for a role ID. It is the
// only role-graph fact layout-assignment specificity needs; the adapter folds
// it behind ResolveLayout so no dialect re-computes assignment scope.
type roleAncestorsFunc func(roleID string) []string

// ContractService adapts MetadataService to the neutral static metadata plane.
// It preserves the decorated/cache schema read path used by v63 describe:
// caller-effective capability facts belong to the policy contract, not this
// adapter.
type ContractService struct {
	metadata      contractMetadataSource
	roleAncestors roleAncestorsFunc
}

// NewContractService builds the neutral metadata adapter. roleAncestors supplies
// the role-graph chain used by ResolveLayout's assignment specificity; it may be
// nil (a pack with no role hierarchy resolves profile/RecordType-only
// assignments) and is called only when a session carries a RoleID.
func NewContractService(metadata contractMetadataSource, roleAncestors roleAncestorsFunc) *ContractService {
	return &ContractService{metadata: metadata, roleAncestors: roleAncestors}
}

func (s *ContractService) Describe(ctx context.Context, objectName string) (*models.ObjectMetadata, error) {
	return s.metadata.GetSchemaOrError(ctx, objectName)
}

func (s *ContractService) ListObjects(ctx context.Context) ([]models.ObjectMetadata, error) {
	schemas := s.metadata.GetSchemas(ctx)
	objects := make([]models.ObjectMetadata, 0, len(schemas))
	for _, schema := range schemas {
		if schema == nil {
			continue
		}
		objects = append(objects, *schema)
	}
	return objects, nil
}

func (s *ContractService) Relationships(ctx context.Context, objectName string) ([]*models.Relationship, error) {
	return s.metadata.GetRelationships(ctx, objectName), nil
}

func (s *ContractService) ChildRelationships(ctx context.Context, objectName string) ([]*models.ObjectMetadata, error) {
	return s.metadata.GetChildRelationships(ctx, objectName), nil
}

func (s *ContractService) KeyPrefix(ctx context.Context, objectName string) (string, bool) {
	return s.metadata.KeyPrefixForObject(ctx, objectName)
}

// ResolveObjectForRecordID exposes the immutable metadata identity owned by
// _System_Object.KeyPrefix. Callers may retain an explicit opaque-ID
// compatibility path, but must not reconstruct a competing prefix registry.
func (s *ContractService) ResolveObjectForRecordID(ctx context.Context, recordID string) (string, bool) {
	return s.metadata.ResolveObjectForRecordID(ctx, recordID)
}

// ResolveLayout folds layout-assignment scope (profile + role-ancestor chain)
// behind the neutral session so dialects never re-derive it. It derives the
// (profileID, roleAncestors) exactly as the legacy describe handler did, then
// delegates to the untouched concrete resolver. The raw substrate layout is
// returned; dialects own the wire projection. nil means "no resolvable layout"
// (the minimal-shell / 404 case the legacy handler also produced).
func (s *ContractService) ResolveLayout(ctx context.Context, objectName string, user *models.UserSession, recordTypeID string) (*models.PageLayout, error) {
	profileID := ""
	var roleAncestors []string
	if user != nil {
		profileID = user.ProfileID
		if user.RoleID != nil && s.roleAncestors != nil {
			roleAncestors = append([]string{*user.RoleID}, s.roleAncestors(*user.RoleID)...)
		} else if user.RoleID != nil {
			roleAncestors = []string{*user.RoleID}
		}
	}
	return s.metadata.ResolveLayout(ctx, objectName, roleAncestors, profileID, recordTypeID), nil
}

// ResolveRelatedListResolution exposes the virtual related-list resolution
// (query/detail/list/create targets) through the neutral contract mirror. The
// CRM-pack rule SSoT stays in the backend pack-rule cache; only the resolved
// shape crosses the seam. The second return is false for non-virtual related
// lists (callers fall back to the display identity).
func (s *ContractService) ResolveRelatedListResolution(ctx context.Context, objectName string, lookupField string) (*contract.RelatedListResolution, bool) {
	resolution := ResolveVirtualRelatedList(objectName, lookupField)
	if resolution == nil {
		return nil, false
	}
	return mapRelatedListResolution(resolution), true
}

func (s *ContractService) Actions(ctx context.Context, objectName string) ([]*models.ActionMetadata, error) {
	return s.metadata.GetActions(ctx, objectName), nil
}

func (s *ContractService) ActionByID(ctx context.Context, actionID string) (*models.ActionMetadata, error) {
	return s.metadata.GetLiveActionByID(ctx, actionID)
}

func (s *ContractService) ListLayouts(ctx context.Context, objectName string) ([]*models.PageLayout, error) {
	source, ok := s.metadata.(contractLayoutMetadataSource)
	if !ok {
		return nil, fmt.Errorf("layout metadata source unavailable")
	}
	return source.GetAllLayouts(ctx, objectName)
}

func (s *ContractService) GetLayoutByID(ctx context.Context, layoutID string) (*models.PageLayout, error) {
	source, ok := s.metadata.(contractLayoutMetadataSource)
	if !ok {
		return nil, fmt.Errorf("layout metadata source unavailable")
	}
	return source.GetLayoutByID(ctx, layoutID)
}

func (s *ContractService) GetLayoutForAuthoring(ctx context.Context, layoutID string) (*models.PageLayout, error) {
	source, ok := s.metadata.(contractLayoutAuthoringSource)
	if !ok {
		return nil, fmt.Errorf("layout authoring source unavailable")
	}
	return source.GetLayoutForAuthoring(ctx, layoutID)
}

// ResolvePicklistValuesForRecordType exposes the record-type-scoped value set on the neutral plane.
//
// It is deliberately not added to contract.Metadata. That interface has nine implementers across two
// repositories, most of them test doubles, and an L1 module cannot compile against a new method until
// its pin moves — so a dialect that needs this capability would be blocked on a release rather than on
// the capability. Discovering it by assertion instead lets a dialect ask whether the deployed platform
// can answer and fail closed when it cannot, which is the honest behaviour for a route whose meaning
// depends on the answer.
//
// The scoping itself is not decided here. This forwards to the resolver that already owns it, so the
// UI API and any other consumer see the same value set, including the master fallback for an object
// with no concrete record types.
func (s *ContractService) ResolvePicklistValuesForRecordType(
	ctx context.Context,
	objectAPIName string,
	recordTypeID string,
	fieldAPIName string,
) (*models.ScopedPicklistValueSet, error) {
	source, ok := s.metadata.(contractPicklistScopeSource)
	if !ok {
		return nil, fmt.Errorf("picklist scope source unavailable")
	}
	return source.ResolvePicklistValuesForRecordType(ctx, objectAPIName, recordTypeID, fieldAPIName)
}

func (s *ContractService) ListLayoutAssignments(ctx context.Context, objectName string) ([]*models.LayoutAssignment, error) {
	source, ok := s.metadata.(contractLayoutMetadataSource)
	if !ok {
		return nil, fmt.Errorf("layout metadata source unavailable")
	}
	return source.GetAssignmentsForObject(ctx, objectName)
}

func (s *ContractService) ListApprovalLayouts(ctx context.Context, objectName string) ([]*models.ApprovalPageLayout, error) {
	source, ok := s.metadata.(contractLayoutMetadataSource)
	if !ok {
		return nil, fmt.Errorf("layout metadata source unavailable")
	}
	return source.GetAllApprovalLayouts(ctx, objectName)
}

func (s *ContractService) GetApprovalLayoutByID(ctx context.Context, layoutID string) (*models.ApprovalPageLayout, error) {
	source, ok := s.metadata.(contractLayoutMetadataSource)
	if !ok {
		return nil, fmt.Errorf("layout metadata source unavailable")
	}
	return source.GetApprovalLayoutByID(ctx, layoutID)
}

func (s *ContractService) ListViews(ctx context.Context, objectName string, user *models.UserSession) ([]*models.ListView, error) {
	source, ok := s.metadata.(contractObjectMetadataCatalogSource)
	if !ok {
		return nil, fmt.Errorf("metadata catalog source unavailable")
	}
	return source.GetListViewsForUser(ctx, objectName, user), nil
}

func (s *ContractService) RecordTypes(ctx context.Context, objectName string) ([]*models.RecordType, error) {
	source, ok := s.metadata.(contractObjectMetadataCatalogSource)
	if !ok {
		return nil, fmt.Errorf("metadata catalog source unavailable")
	}
	return source.GetRecordTypes(ctx, objectName)
}

func mapRelatedListResolution(r *RelatedListResolution) *contract.RelatedListResolution {
	out := &contract.RelatedListResolution{
		Query: contract.ResolvedQueryTarget{
			ObjectApiName:      r.Query.ObjectApiName,
			LookupField:        r.Query.LookupField,
			ParentFieldApiName: r.Query.ParentFieldApiName,
		},
		Detail: contract.ResolvedDetailTarget{
			ObjectApiName: r.Detail.ObjectApiName,
		},
		List: contract.ResolvedListTarget{
			ObjectApiName:      r.List.ObjectApiName,
			FilterField:        r.List.FilterField,
			ParentFieldApiName: r.List.ParentFieldApiName,
		},
		Create: contract.ResolvedCreateTarget{
			Kind:          r.Create.Kind,
			ObjectApiName: r.Create.ObjectApiName,
		},
	}
	if len(r.Create.PrefillMappings) > 0 {
		out.Create.PrefillMappings = make(map[string]string, len(r.Create.PrefillMappings))
		for k, v := range r.Create.PrefillMappings {
			out.Create.PrefillMappings[k] = v
		}
	}
	return out
}
