package packs

import (
	"fmt"
	"strings"

	"github.com/nexuscrm/files"
	"github.com/nexuscrm/metadata"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/searchmetadata"
	cp "github.com/pingcap-inc/nexuscrm-contract/pkg/controlplane"
)

// externalBuildRoute wraps a contract-only cp.ComponentBuildHandler so the
// engine dispatch can drive it with a cp.ComponentBuildRequest while the handler
// mutates the engine build context (which implements the cp build interfaces).
type externalBuildRoute struct {
	handler      cp.ComponentBuildHandler
	registration *cp.ComponentKindRegistration
	lifecycle    *cp.ComponentManagedLifecycleRegistration
}

// newExternalBuildRoutes folds a provider's registrations into the engine's
// external-route map, diverting ONLY the kinds named in `only`. The install
// gate is preserved: a registration whose capability is not installable, or
// whose build handler is nil, is skipped (so the kind falls through to the
// internal dispatch and its own install gate). All kinds absent from `only`
// keep the internal method-value path untouched.
func newExternalBuildRoutes(regs []cp.ComponentKindRegistration, only map[ComponentKind]struct{}) map[ComponentKind]externalBuildRoute {
	out := map[ComponentKind]externalBuildRoute{}
	for _, r := range regs {
		k := ComponentKind(r.Contract.Kind)
		if _, want := only[k]; !want {
			continue
		}
		if !r.Capability.Installable || r.BuildHandler == nil {
			continue
		}
		registration := cloneComponentKindRegistration(r)
		out[k] = externalBuildRoute{handler: registration.BuildHandler, registration: &registration}
	}
	return out
}

// Compile-time assertion: the engine build context implements both contract
// build interfaces, so an external handler can type-assert it to the
// presentation sink (exactly as the real provider's presentation() helper does).
var (
	_ cp.ComponentBuildContext             = (*sourceBackedComponentBuildContext)(nil)
	_ cp.PresentationComponentBuildContext = (*sourceBackedComponentBuildContext)(nil)
	_ cp.PresentationComponentBuildContext = (*managedOnlyComponentBuildContext)(nil)
)

// managedOnlyComponentBuildContext is the output boundary for a managed
// registration. It exposes the contract's full build-context shape so handler
// assertions remain deterministic, but every legacy sink fails before touching
// pack state. Only AddManagedComponent delegates to the retained descriptor.
type managedOnlyComponentBuildContext struct {
	delegate *sourceBackedComponentBuildContext
}

func (b *managedOnlyComponentBuildContext) PackName() string {
	return b.delegate.PackName()
}

func (b *managedOnlyComponentBuildContext) LookupObjectSchema(apiName string) (cp.ComponentObjectSchema, bool) {
	return b.delegate.LookupObjectSchema(apiName)
}

func (b *managedOnlyComponentBuildContext) AddManagedComponent(component cp.ComponentBuildManagedComponent) error {
	return b.delegate.AddManagedComponent(component)
}

func (b *managedOnlyComponentBuildContext) rejectLegacyOutput(output string) error {
	err := fmt.Errorf("managed component %s/%s cannot emit legacy %s", b.delegate.currentComponent.Kind, b.delegate.currentComponent.BusinessKey, output)
	if b.delegate.currentManagedBuildErr == nil {
		b.delegate.currentManagedBuildErr = err
	}
	return err
}

func (b *managedOnlyComponentBuildContext) AddRuntimeStartupShallowDeclaration(cp.ComponentKind, string, []byte) error {
	return b.rejectLegacyOutput("runtime-startup shallow declaration")
}

func (b *managedOnlyComponentBuildContext) AddRelatedListRuleComponent(cp.ComponentBuildRequest, []cp.RelatedListRule) error {
	return b.rejectLegacyOutput("related-list-rule component")
}

func (b *managedOnlyComponentBuildContext) AddPageLayoutComponent(cp.ComponentBuildRequest, cp.PageLayoutDeclaration) error {
	return b.rejectLegacyOutput("page-layout component")
}

func (b *managedOnlyComponentBuildContext) AddLayoutAssignmentComponent(cp.ComponentBuildRequest, []cp.LayoutAssignmentDeclaration) error {
	return b.rejectLegacyOutput("layout-assignment component")
}

func (b *managedOnlyComponentBuildContext) AddReportTypeComponent(cp.ComponentBuildRequest, []cp.ReportTypeDeclaration) error {
	return b.rejectLegacyOutput("report-type component")
}

func (b *managedOnlyComponentBuildContext) AddUIActionComponent(cp.ComponentBuildRequest, []cp.UIActionDeclaration) error {
	return b.rejectLegacyOutput("UI-action component")
}

func (b *managedOnlyComponentBuildContext) AddSearchScopeOrderComponent(cp.ComponentBuildRequest, []cp.SearchScopeOrderItem) error {
	return b.rejectLegacyOutput("search-scope-order component")
}

func (b *managedOnlyComponentBuildContext) AddSearchAllDefaultFieldsComponent(cp.ComponentBuildRequest, []cp.SearchAllDefaultFieldsDeclaration) error {
	return b.rejectLegacyOutput("search-all-default-fields component")
}

func (b *managedOnlyComponentBuildContext) AddHrefRewriteFieldsComponent(cp.ComponentBuildRequest, []cp.HrefRewriteField) error {
	return b.rejectLegacyOutput("href-rewrite-fields component")
}

// --- cp.ComponentBuildContext (base) -----------------------------------------

func (b *sourceBackedComponentBuildContext) PackName() string {
	return b.p.Name()
}

// LookupObjectSchema is a faithful stub for this slice: cp.ComponentObjectSchema
// is an empty contract stub and no routed kind consumes it yet.
func (b *sourceBackedComponentBuildContext) LookupObjectSchema(string) (cp.ComponentObjectSchema, bool) {
	return cp.ComponentObjectSchema{}, false
}

func (b *sourceBackedComponentBuildContext) AddManagedComponent(component cp.ComponentBuildManagedComponent) error {
	if err := component.Validate(); err != nil {
		b.currentManagedBuildErr = err
		return b.currentManagedBuildErr
	}
	b.currentManagedDescriptorCount++
	if b.currentManagedDescriptorCount != 1 {
		b.currentManagedBuildErr = fmt.Errorf("component %s/%s added more than one managed descriptor", b.currentComponent.Kind, b.currentComponent.BusinessKey)
		return b.currentManagedBuildErr
	}
	if b.currentExternalRoute == nil || b.currentExternalRoute.registration == nil ||
		(b.currentExternalRoute.registration.ManagedProvider == nil && b.currentExternalRoute.lifecycle == nil) {
		b.currentManagedBuildErr = fmt.Errorf("managed component declarations are not wired for %s/%s: no managed registration", b.currentComponent.Kind, b.currentComponent.BusinessKey)
		return b.currentManagedBuildErr
	}
	registration := cloneComponentKindRegistration(*b.currentExternalRoute.registration)
	if ComponentKind(registration.Contract.Kind) != b.currentComponent.Kind {
		b.currentManagedBuildErr = fmt.Errorf("managed registration kind %q does not match component kind %q", registration.Contract.Kind, b.currentComponent.Kind)
		return b.currentManagedBuildErr
	}
	var managed *sourceBackedExternalManagedComponent
	if b.currentExternalRoute.lifecycle == nil {
		managed = newSourceBackedExternalManagedComponent(
			b.p.Name(), b.currentComponent, b.currentPayload, component, registration,
		)
	} else {
		managed = newSourceBackedExternalManagedComponent(
			b.p.Name(), b.currentComponent, b.currentPayload, component, registration, *b.currentExternalRoute.lifecycle,
		)
	}
	managed.effects = b.p.componentRegistrationSet.ManagedLifecycleEffectAdapters()
	b.p.components = append(b.p.components, managed)
	return nil
}

// AddRuntimeStartupShallowDeclaration bridges to the engine helper, matching its
// trim/drop-empty semantics. Not exercised by search_scope_order (it emits no
// shallow rows) but implemented faithfully so the next presentation kind reuses it.
func (b *sourceBackedComponentBuildContext) AddRuntimeStartupShallowDeclaration(kind cp.ComponentKind, businessKey string, projection []byte) error {
	b.p.appendRuntimeStartupShallowDeclaration(ComponentKind(kind), businessKey, string(projection))
	return nil
}

// --- cp.PresentationComponentBuildContext (8 sinks) --------------------------

// AddSearchScopeOrderComponent is the ONE real sink wired in this slice. It
// reconstructs the SAME internal sourceBackedManagedComponent the engine builds
// today, from the RAW payload bytes (req.Payload, never re-marshalled) and the
// captured engine ManifestComponent (currentComponent, which carries DependsOn).
// All side-effects of the internal buildSearchScopeOrder are reproduced in order.
func (b *sourceBackedComponentBuildContext) AddSearchScopeOrderComponent(req cp.ComponentBuildRequest, decls []cp.SearchScopeOrderItem) error {
	items := make([]metadata.SearchScopeOrderItem, len(decls))
	for i, d := range decls {
		items[i] = metadata.SearchScopeOrderItem{Name: d.Name, Label: d.Label}
	}
	packComponent := newSearchScopeOrderManagedComponentFromItems(b.p.Name(), b.currentComponent, req.Payload, items)
	b.p.searchScopeOrder = append(b.p.searchScopeOrder, items...)
	if err := metadata.ValidatePackSearchScopeOrder(b.p.searchScopeOrder); err != nil {
		return err
	}
	if err := metadata.ValidatePackSearchScopeOrderOwnership(b.p.searchScopeOrder, b.p.ownedObjectAPINames); err != nil {
		return err
	}
	b.p.components = append(b.p.components, packComponent)
	return nil
}

// The remaining 7 presentation sinks are required to satisfy the interface but
// are NOT routed in this slice. They fail loud rather than silently no-op, so an
// accidental future routing of one of these kinds surfaces immediately instead
// of double-pathing the build.

// AddRelatedListRuleComponent reconstructs the SAME internal component the engine
// buildRelatedListRule produces, from the typed []cp.RelatedListRule the provider
// emits plus the RAW payload bytes (req.Payload, never re-marshalled) and the
// captured engine ManifestComponent (currentComponent). The cp.RelatedListRule
// declaration is a NEUTRAL PROJECTION that OMITS the internal type's Resolution
// (*RelatedListResolution) and RuntimeAppendOnly (json:"-") fields; rules that
// carry a "resolution" object would round-trip lossily. The SF corpus declares
// neither, so the reconstructed rule slice matches; the engine's raw-byte
// ExpectedPayload is what the deep-compare freezes. All side-effects of the
// internal builder are reproduced: validate, then append the component.
func (b *sourceBackedComponentBuildContext) AddRelatedListRuleComponent(req cp.ComponentBuildRequest, decls []cp.RelatedListRule) error {
	rules := make([]metadata.RelatedListRule, len(decls))
	for i, d := range decls {
		rules[i] = metadata.RelatedListRule{
			Key:              d.Key,
			ParentObjectAPI:  d.ParentObjectAPI,
			ChildObjectAPI:   d.ChildObjectAPI,
			LookupField:      d.LookupField,
			Bucket:           metadata.Bucket(d.Bucket),
			SortOrder:        d.SortOrder,
			AppendIfMissing:  d.AppendIfMissing,
			Label:            d.Label,
			SecondaryLabel:   d.SecondaryLabel,
			LookupFieldLabel: d.LookupFieldLabel,
			Fields:           append([]string(nil), d.Fields...),
		}
	}
	if err := metadata.ValidatePackRelatedListRules(rules); err != nil {
		return err
	}
	b.p.components = append(b.p.components, newRelatedListRuleManagedComponentFromRules(b.p.Name(), b.currentComponent, req.Payload, rules))
	return nil
}

// AddPageLayoutComponent reconstructs the SAME internal sourceBackedManagedComponent
// the engine buildPageLayout produces, from the RAW payload bytes (req.Payload,
// never re-marshalled — cp.PageLayoutDeclaration.Config carries the same bytes but
// we use req.Payload to guarantee verbatim identity) and the captured engine
// ManifestComponent (currentComponent, which carries DependsOn). The page_layout
// build component sets NO expectedProjection (ExpectedProjection()==nil); its
// compared surface is exactly {raw payload, manifest fields}. The relatedListRules
// the constructor closes over are L0-engine-resident (b.p.relatedListRules) and are
// touched ONLY at apply/verify, never on the build surface, so reading the engine's
// own slice here is faithful and the declaration need not carry them. All
// side-effects of the internal buildPageLayout are reproduced in order: the
// non-default batch mutation (a no-op on the SF corpus, all 14 are default) then the
// component append.
func (b *sourceBackedComponentBuildContext) AddPageLayoutComponent(req cp.ComponentBuildRequest, _ cp.PageLayoutDeclaration) error {
	layout, err := sourceBackedPageLayoutFromPayload(req.Payload, b.currentComponent.BusinessKey, componentPayloadLabel(b.currentComponent))
	if err != nil {
		return err
	}
	if !layout.IsDefault {
		b.pageLayoutBatch.declaredNonDefault = append(b.pageLayoutBatch.declaredNonDefault, sourceBackedPageLayoutBusinessKey{
			ObjectAPIName: layout.ObjectAPIName,
			LayoutName:    layout.LayoutName,
		})
	}
	packComponent, err := newSourceBackedPageLayoutComponent(b.p.Name(), b.currentComponent, req.Payload, b.p.relatedListRules)
	if err != nil {
		return err
	}
	b.p.components = append(b.p.components, packComponent)
	return nil
}

// AddLayoutAssignmentComponent attempts to reconstruct the internal
// layout_assignment component FROM THE CONTRACT DECLARATION ALONE
// ([]cp.LayoutAssignmentDeclaration = {ObjectAPIName, RecordType}) plus the raw
// payload and the captured engine ManifestComponent (currentComponent, carrying
// DependsOn). This sink is DELIBERATELY NOT added to any routed `only` set: it is
// retained for symmetry and to make the DRIFT provable.
//
// The internal buildLayoutAssignment's ExpectedProjection
// ([]sourceBackedLayoutAssignmentRow) carries a RecordTypeID resolved from
// engine-only sibling state — b.recordTypeIndex[key].expectedID, set inside
// validateSourceBackedLayoutAssignmentComponent (source_backed_layout_assignment_component.go:224-235).
// cp.LayoutAssignmentDeclaration carries NEITHER that id NOR any field from which it
// is derivable. So a reconstruction that consumes ONLY the declaration produces rows
// whose RecordTypeID is empty: the declaration is LOSSY for this kind. Reproducing
// fidelity would require this sink to re-reach into b.recordTypeIndex and re-run the
// engine-only resolution — exactly the engine-resident logic the BUILD-path migration
// is meant to externalize — which is why the kind is kept internal.
func (b *sourceBackedComponentBuildContext) AddLayoutAssignmentComponent(req cp.ComponentBuildRequest, decls []cp.LayoutAssignmentDeclaration) error {
	layoutBusinessKey, layoutID, _, err := sourceBackedLayoutAssignmentTarget(b.currentComponent)
	if err != nil {
		return err
	}
	componentBusinessKey := strings.TrimSpace(b.currentComponent.BusinessKey)
	ownerPack := b.p.Name()
	rows := make([]sourceBackedLayoutAssignmentRow, 0, len(decls))
	for _, d := range decls {
		objectAPIName := strings.TrimSpace(d.ObjectAPIName)
		recordType := strings.TrimSpace(d.RecordType)
		prefix := sourceBackedLayoutAssignmentComponentIDPrefix(ownerPack, componentBusinessKey, objectAPIName)
		rows = append(rows, sourceBackedLayoutAssignmentRow{
			ComponentBusinessKey: componentBusinessKey,
			AssignmentID:         generatedSourceBackedLayoutAssignmentID(ownerPack, componentBusinessKey, objectAPIName, recordType),
			AssignmentIDPrefix:   prefix,
			ObjectAPIName:        objectAPIName,
			RecordTypeName:       recordType,
			// RecordTypeID is INTENTIONALLY left empty: the cp declaration cannot
			// carry the engine-only b.recordTypeIndex[key].expectedID. This is the
			// lossy field that drives the DRIFT.
			LayoutBusinessKey: layoutBusinessKey,
			LayoutID:          layoutID,
			RouteSource:       models.LayoutAssignmentRouteSourceManagedSeed,
			OwnerPack:         ownerPack,
		})
	}
	sortSourceBackedLayoutAssignmentRows(rows)
	expected := rows
	b.p.components = append(b.p.components, &sourceBackedManagedComponent{
		packName:          ownerPack,
		manifestComponent: b.currentComponent,
		payload:           req.Payload,
		expectedProjection: func() (any, error) {
			return expected, nil
		},
	})
	return nil
}

// AddReportTypeComponent reconstructs the SAME internal component the engine
// buildReportType produces, from the typed []cp.ReportTypeDeclaration the
// provider emits (already trimmed/deduped/sorted) plus the RAW payload bytes and
// the captured engine ManifestComponent. cp.ReportTypeDeclaration is field-for-
// field 1:1 with sourceBackedReportTypeSeed (RawMessage sub-objects preserved),
// so the reconstruction is loss-free. The internal builder's canonicalization
// applies a Label=FullName fallback for empty labels; that is reproduced here so
// the apply/verify seeds and the runtime-startup shallow keys match exactly. All
// side-effects of the internal builder are reproduced in order: emit a shallow
// key per FullName, then append the component.
func (b *sourceBackedComponentBuildContext) AddReportTypeComponent(req cp.ComponentBuildRequest, decls []cp.ReportTypeDeclaration) error {
	parsed := sourceBackedReportTypesPayload{ReportTypes: make([]sourceBackedReportTypeSeed, len(decls))}
	for i, d := range decls {
		label := d.Label
		if label == "" {
			label = d.FullName
		}
		parsed.ReportTypes[i] = sourceBackedReportTypeSeed{
			FullName:      d.FullName,
			Label:         label,
			Description:   d.Description,
			BaseObject:    d.BaseObject,
			Deployed:      d.Deployed,
			Autogenerated: d.Autogenerated,
			Sections:      d.Sections,
			Categories:    d.Categories,
			Join:          d.Join,
			Metadata:      d.Metadata,
		}
	}
	for _, reportType := range parsed.ReportTypes {
		b.p.appendRuntimeStartupShallowKey(ComponentKindReportType, reportType.FullName)
	}
	b.reportTypeBatch.append(parsed)
	b.p.components = append(b.p.components, newReportTypeManagedComponentFromPayload(b.p.Name(), b.currentComponent, req.Payload, parsed))
	return nil
}

// AddUIActionComponent reconstructs the SAME enriched sourceBackedManagedComponent
// the internal buildUIAction produces, from the post-fan-out
// []cp.UIActionDeclaration the external handler parsed plus the RAW payload bytes
// (req.Payload, never re-marshalled) and the captured engine ManifestComponent
// (currentComponent, which carries DependsOn — the public cp.ManifestComponent in
// req.Component omits it). All side-effects of the internal buildUIAction are
// reproduced in order: the projection rows are derived FROM the declarations (so a
// corrupted declaration field flows into ExpectedProjection), and the
// runtime-startup shallow keys are emitted per sorted row.
func (b *sourceBackedComponentBuildContext) AddUIActionComponent(req cp.ComponentBuildRequest, decls []cp.UIActionDeclaration) error {
	rows, err := standardActionRowsFromDeclarations(b.p.Name(), decls)
	if err != nil {
		return err
	}
	packComponent := newUIActionManagedComponentFromRows(b.p.Name(), b.currentComponent, req.Payload, rows)
	for _, row := range rows {
		b.p.appendRuntimeStartupShallowKey(ComponentKindUIAction, JoinComponentBusinessKey(row.ObjectAPIName, row.Name))
	}
	b.p.components = append(b.p.components, packComponent)
	return nil
}

// AddSearchAllDefaultFieldsComponent reconstructs the SAME internal component the
// engine buildSearchAllDefaultFields produces, from the typed
// []cp.SearchAllDefaultFieldsDeclaration the provider emits plus the RAW payload
// bytes and the captured engine ManifestComponent. cp.SearchAllDefaultFieldsDeclaration
// is field-for-field 1:1 with searchmetadata.SearchAllDefaultFieldsDeclaration, so
// the reconstruction is loss-free. The internal builder validates inside the
// projection helper; we re-validate the reconstructed slice to preserve the same
// fail-closed behavior, then append the component. NOTE: the SF corpus declares
// NO search_all_default_fields component, so this sink is implemented and routed
// for completeness but is not exercised by the SF deep-compare.
func (b *sourceBackedComponentBuildContext) AddSearchAllDefaultFieldsComponent(req cp.ComponentBuildRequest, decls []cp.SearchAllDefaultFieldsDeclaration) error {
	declarations := make([]searchmetadata.SearchAllDefaultFieldsDeclaration, len(decls))
	for i, d := range decls {
		declarations[i] = searchmetadata.SearchAllDefaultFieldsDeclaration{
			ObjectAPIName: d.ObjectAPIName,
			Fields:        append([]string(nil), d.Fields...),
		}
	}
	if err := searchmetadata.ValidatePackSearchAllDefaultFields(declarations); err != nil {
		return err
	}
	b.p.components = append(b.p.components, newSearchAllDefaultFieldsManagedComponentFromDeclarations(b.p.Name(), b.currentComponent, req.Payload, declarations))
	return nil
}

// AddHrefRewriteFieldsComponent reconstructs the SAME internal component the
// engine buildHrefRewriteFields produces, from the typed []cp.HrefRewriteField
// the provider emits plus the RAW payload bytes and the captured engine
// ManifestComponent. cp.HrefRewriteField is field-for-field 1:1 with
// files.HrefRewriteField, so the reconstruction is loss-free. All side-effects of
// the internal builder are reproduced in order: re-run the trim/dedupe validator,
// validate against the pack object specs, register the fields on the pack, then
// emit one runtime-startup shallow declaration per field, and finally append the
// component (whose ExpectedProjection carries the parsed field slice).
func (b *sourceBackedComponentBuildContext) AddHrefRewriteFieldsComponent(req cp.ComponentBuildRequest, decls []cp.HrefRewriteField) error {
	mapped := make([]files.HrefRewriteField, len(decls))
	for i, d := range decls {
		mapped[i] = files.HrefRewriteField{
			ParentApiName: d.ParentApiName,
			FieldApiName:  d.FieldApiName,
		}
	}
	fields, err := validateHrefRewriteFields(mapped, componentPayloadLabel(b.currentComponent))
	if err != nil {
		return err
	}
	if err := validateHrefRewriteFieldsForObjectSpecs(fields, b.packObjectSpecs); err != nil {
		return err
	}
	b.p.hrefRewriteFields = append(b.p.hrefRewriteFields, fields...)
	for _, field := range fields {
		projection, err := hrefRewriteFieldStartupProjection(field)
		if err != nil {
			return fmt.Errorf("href_rewrite_fields %s/%s startup shallow projection: %w", field.ParentApiName, field.FieldApiName, err)
		}
		b.p.appendRuntimeStartupShallowDeclaration(ComponentKindHrefRewriteFields, JoinComponentBusinessKey(field.ParentApiName, field.FieldApiName), projection)
	}
	b.p.components = append(b.p.components, newHrefRewriteFieldsManagedComponentFromFields(b.p.Name(), b.currentComponent, req.Payload, fields))
	return nil
}
