package analytics

import (
	"context"
	"fmt"
	"runtime/debug"
	"strings"

	"github.com/nexuscrm/backend/internal/features/metadata"
	pkgerrors "github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/backend/pkg/logging"
	"github.com/nexuscrm/shared/pkg/models"
)

// CRTResolver returns a synthesized *JoinTemplate for a given Report.SourceObject
// when the source refers to a Custom Report Type (stored in _System_ReportType
// or encoded as a synthetic "CustomEntity$..." string).
//
// Contract:
//   - (nil, nil, nil)      — SourceObject is not a CRT; caller must fall
//     through to single-table handling.
//   - (tmpl, warnings, nil) — template ready for executeJoinReport; warnings
//     is always a non-nil slice when tmpl is non-nil (may be empty).
//   - (nil, nil, err)      — infrastructure failure the caller should surface
//     as 5xx (currently only DB read errors from the loader).
//
// See docs/design/crt-report-support.md §4.1 §4.3 §4.10.
type CRTResolver interface {
	Resolve(ctx context.Context, sourceObject string, user *models.UserSession) (*JoinTemplate, []CRTWarning, error)
}

type crtResolver struct {
	loader       CRTRepository
	synthesizer  *crtSynthesizer
	parser       *syntheticParser
	standardObjs *StandardObjectResolver
	// metadata is the narrow schema lookup seam. Production wires it to a
	// crtMetadataAdapter wrapping *metadata.MetadataService; tests can
	// inject a hand-built fake via newCRTResolverForTest without spinning
	// up the full metadata pipeline.
	metadata crtMetadataLookup
	logger   *logging.Logger
}

// NewCRTResolver constructs the default resolver. Any of loader, md, std,
// logger may be nil — the resolver degrades gracefully and behaves as
// "not a CRT" for every input.
func NewCRTResolver(loader CRTRepository, md *metadata.MetadataService, std *StandardObjectResolver, logger *logging.Logger) CRTResolver {
	return &crtResolver{
		loader:       loader,
		synthesizer:  newCRTSynthesizer(md, logger),
		parser:       &syntheticParser{},
		standardObjs: std,
		metadata:     newCRTMetadataAdapter(md),
		logger:       logger,
	}
}

// newCRTResolverForTest wires the resolver against a hand-built
// crtMetadataLookup fake so unit tests can exercise the §4.10
// fault-tolerance matrix without standing up a real
// *metadata.MetadataService. Not exported — callers outside tests must use
// NewCRTResolver.
func newCRTResolverForTest(loader CRTRepository, md crtMetadataLookup, std *StandardObjectResolver, logger *logging.Logger) *crtResolver {
	return &crtResolver{
		loader:       loader,
		synthesizer:  newCRTSynthesizerWithLookup(md, logger),
		parser:       &syntheticParser{},
		standardObjs: std,
		metadata:     md,
		logger:       logger,
	}
}

// Resolve implements CRTResolver. Panics in any downstream component are
// recovered and treated as "not a CRT" — matches §4.10 row C8. The only err
// return path is infrastructure failure from the loader; everything else is
// either a successful template or a graceful degrade-plus-warning.
func (r *crtResolver) Resolve(ctx context.Context, sourceObject string, user *models.UserSession) (tmpl *JoinTemplate, warnings []CRTWarning, err error) {
	defer func() {
		if rec := recover(); rec != nil {
			if r.logger != nil {
				r.logger.Error(fmt.Sprintf("crt resolver panic recovered for %q: %v\n%s", sourceObject, rec, debug.Stack()), nil)
			}
			tmpl, warnings, err = nil, nil, nil
		}
	}()

	sourceObject = strings.TrimSpace(sourceObject)
	if sourceObject == "" {
		return nil, nil, nil
	}

	// (1) Short-circuit for known standard object names so we never route a
	// plain "Lead" or "OrderItemList" through the CRT machinery.
	if r.standardObjs != nil && r.standardObjs.Lookup(sourceObject) != "" {
		return nil, nil, nil
	}

	// (2) Shadow the existing hardcoded JoinTemplate map — the legacy
	// fast path stays untouched.
	if LookupJoinTemplate(sourceObject) != nil {
		return nil, nil, nil
	}

	// (3) Stored CRT: try _System_ReportType first. Loader returns
	// NotFound when the row does not exist; we treat that as "try
	// the synthetic path next" and only propagate unexpected errors.
	if r.loader != nil {
		crt, loadErr := r.loader.LoadByFullName(ctx, sourceObject, user)
		if loadErr == nil && crt != nil {
			t, w := r.synthesizer.Synthesize(ctx, crt)
			if t == nil {
				// Synthesizer refused the row (e.g. missing base schema).
				// Fall through to single-table — caller decides what to do
				// with the CRT name.
				return nil, nil, nil
			}
			if w == nil {
				w = []CRTWarning{}
			}
			return t, w, nil
		}
		if loadErr != nil && !pkgerrors.IsNotFound(loadErr) {
			return nil, nil, fmt.Errorf("crt resolver: load by full name %q: %w", sourceObject, loadErr)
		}
	}

	// (4) Synthetic CRT: parse the encoded form and build a template
	// against live metadata. The fault matrix is documented in §4.10.
	parsed := r.parser.Parse(sourceObject)
	switch parsed.shape {
	case shapeUnknown:
		return nil, nil, nil
	case shapeCustomEntity:
		return r.synthesizeSynthetic(ctx, sourceObject, parsed, false)
	case shapeCustomEntityScoped, shapeOrderCustomEntity:
		return r.synthesizeSynthetic(ctx, sourceObject, parsed, true)
	case shapeNestedCustomEntity:
		return r.synthesizeNested(ctx, sourceObject, parsed)
	default:
		return nil, nil, nil
	}
}

// synthesizeSynthetic handles shapeCustomEntity, shapeCustomEntityScoped,
// and shapeOrderCustomEntity rows of the fault matrix. Cases C1-C4.
func (r *crtResolver) synthesizeSynthetic(ctx context.Context, sourceObject string, parsed parsedSynthetic, wantAnchor bool) (*JoinTemplate, []CRTWarning, error) {
	baseSchema := r.schemaFor(ctx, parsed.baseObject)
	if baseSchema == nil {
		// C4: base object does not exist; pretend we never saw it so the
		// single-table fallthrough has a chance.
		return nil, nil, nil
	}

	tmpl := &JoinTemplate{
		PrimaryTable: strings.ToLower(baseSchema.APIName),
		PrimaryAlias: primaryAliasFor(baseSchema.APIName),
		Columns:      make(map[string]QualifiedColumn),
	}

	if !wantAnchor || parsed.anchorObject == "" {
		// shapeCustomEntity: base-only template.
		return tmpl, []CRTWarning{}, nil
	}

	anchorSchema := r.schemaFor(ctx, parsed.anchorObject)
	if anchorSchema == nil {
		// C3: anchor object does not exist → base-only + warning.
		return tmpl, []CRTWarning{
			{
				Code:         WarnCRTAnchorObjectMissing,
				Message:      fmt.Sprintf("anchor object %q not found for %q", parsed.anchorObject, sourceObject),
				SourceObject: sourceObject,
			},
		}, nil
	}

	if !hasField(baseSchema, parsed.anchorField) {
		// C2: anchor exists but the FK column is missing.
		return tmpl, []CRTWarning{
			{
				Code:         WarnCRTAnchorFKMissing,
				Message:      fmt.Sprintf("anchor FK %q missing on base %q for %q", parsed.anchorField, parsed.baseObject, sourceObject),
				SourceObject: sourceObject,
			},
		}, nil
	}

	// C1: two-table template. LEFT JOIN so we never drop base rows when the
	// anchor is unset (see §4.6 synthetic LEFT-JOIN default).
	anchorAlias := childAliases[0]
	tmpl.Joins = append(tmpl.Joins, JoinStep{
		Table:    strings.ToLower(anchorSchema.APIName),
		Alias:    anchorAlias,
		JoinType: "LEFT JOIN",
		OnLeft:   fmt.Sprintf("%s.%s", tmpl.PrimaryAlias, parsed.anchorField),
		OnRight:  fmt.Sprintf("%s.Id", anchorAlias),
	})
	return tmpl, []CRTWarning{}, nil
}

// synthesizeNested handles shapeNestedCustomEntity — rows C5 and C6.
func (r *crtResolver) synthesizeNested(ctx context.Context, sourceObject string, parsed parsedSynthetic) (*JoinTemplate, []CRTWarning, error) {
	baseSchema := r.schemaFor(ctx, parsed.baseObject)
	if baseSchema == nil {
		// C6: base absent → not our problem.
		return nil, nil, nil
	}
	tmpl := &JoinTemplate{
		PrimaryTable: strings.ToLower(baseSchema.APIName),
		PrimaryAlias: primaryAliasFor(baseSchema.APIName),
		Columns:      make(map[string]QualifiedColumn),
	}
	return tmpl, []CRTWarning{
		{
			Code:         WarnCRTNestedUnsupported,
			Message:      fmt.Sprintf("nested CRT %q rendered as base object %q only", sourceObject, parsed.baseObject),
			SourceObject: sourceObject,
		},
	}, nil
}

func (r *crtResolver) schemaFor(ctx context.Context, apiName string) *models.ObjectMetadata {
	if r.metadata == nil || apiName == "" {
		return nil
	}
	return r.metadata.GetSchema(ctx, apiName)
}

// hasField reports whether the schema exposes a field whose API name matches
// the candidate (case-insensitive). Used for FK verification on synthetic
// anchor edges.
func hasField(schema *models.ObjectMetadata, name string) bool {
	if schema == nil || name == "" {
		return false
	}
	if schema.FieldsMap != nil {
		if _, ok := schema.FieldsMap[name]; ok {
			return true
		}
		for k := range schema.FieldsMap {
			if strings.EqualFold(k, name) {
				return true
			}
		}
	}
	for i := range schema.Fields {
		if strings.EqualFold(schema.Fields[i].APIName, name) {
			return true
		}
		if strings.EqualFold(schema.Fields[i].PhysicalName, name) {
			return true
		}
	}
	return false
}
