// Package autonumber implements metadata-driven identifier assignment for NexusCRM.
package autonumber

import (
	"context"
	"fmt"

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

// RecordLookup is the minimal repository surface the default
// CrossRecordResolver needs to walk FK chains. It matches
// ports.RecordRepository.FindOne so the engine wire-up can pass the
// existing record repo without a new adapter.
type RecordLookup interface {
	FindOne(ctx context.Context, objectAPIName string, id string) (models.SObject, error)
}

// NewRepoCrossRecordResolver constructs a CrossRecordResolver backed by a
// RecordLookup (typically the application's ports.RecordRepository). Walks
// each chain step by step:
//
//	row := rootRecord
//	for each step:
//	    fkVal := row[step.FK]       // defaults to step.TargetObject+"Id" if empty
//	    row = lookup.FindOne(step.TargetObject, fkVal)
//	return row
//
// The engine's resolveCrossRecords is responsible for extracting
// step.TargetField from the final row. The resolver returns two distinct
// error classes so the multi-chain classifier can keep them apart:
//   - ErrTokenResolve — user-data condition (null FK, missing parent row).
//   - ErrChainResolverFailure — infrastructure condition (FindOne DB error,
//     wiring bug). The user cannot fix this by editing record data.
//
// This split matches the fail-closed semantics of the legacy
// compound_autonumber_listener while letting the HTTP layer return 400
// vs 500 correctly even when one chain in a multi-chain token has a
// user-data condition and a sibling chain has an infra failure.
//
// `readStrategy` is accepted for API parity but currently ignored — both
// `caller_txn` and `fresh_read` paths use the same RecordLookup. The v3.1
// cutover seeds (PRD §8.1) do not require fresh-read isolation; adding it
// is a v3.2 concern.
func NewRepoCrossRecordResolver(lookup RecordLookup) CrossRecordResolver {
	return &repoResolver{lookup: lookup}
}

type repoResolver struct {
	lookup RecordLookup
}

// resolveTargetObjectTable maps a parser-derived logical object name
// (as it appears inside the format token, e.g. `{RecordType.X}`) to
// the physical table the FindOne repo should read. NexusCRM keeps
// RecordType data in `_System_RecordType`; the sibling `RecordType`
// table is the SF-compat surface but is empty by default in this
// cutover, so a naive FindOne("RecordType", id) would miss every row.
// Additions here should be rare — they represent genuine logical ↔
// physical naming splits, not schema bugs.
func resolveTargetObjectTable(logical string) string {
	switch logical {
	case "RecordType":
		return "_System_RecordType"
	default:
		return logical
	}
}

// LoadTarget implements CrossRecordResolver.LoadTarget.
func (r *repoResolver) LoadTarget(
	ctx context.Context,
	_ string, // readStrategy — see NewRepoCrossRecordResolver doc
	rootRecord models.SObject,
	chain []CrossRecordStep,
) (models.SObject, error) {
	// Wiring / programmer errors — surface as infra so the multi-chain
	// classifier in resolveSingleRef does not treat them as user data.
	if len(chain) == 0 {
		return nil, fmt.Errorf("%w: empty chain", ErrChainResolverFailure)
	}
	if r.lookup == nil {
		return nil, fmt.Errorf("%w: nil lookup", ErrChainResolverFailure)
	}

	cur := rootRecord
	for i, step := range chain {
		fkCol := step.FK
		if fkCol == "" {
			// Default: "<TargetObject>Id" (same heuristic as the
			// fake resolver / legacy listener).
			fkCol = step.TargetObject + "Id"
		}
		fkRaw, ok := cur[fkCol]
		if !ok || fkRaw == nil {
			// User-data condition: the FK on the input record is null.
			return nil, fmt.Errorf("%w: step %d FK %q is nil on current row",
				ErrTokenResolve, i, fkCol)
		}
		fkStr, ok := fkRaw.(string)
		if !ok || fkStr == "" {
			// User-data condition: the FK is the wrong type or empty.
			return nil, fmt.Errorf("%w: step %d FK %q is empty/non-string",
				ErrTokenResolve, i, fkCol)
		}
		lookupTable := resolveTargetObjectTable(step.TargetObject)
		next, err := r.lookup.FindOne(ctx, lookupTable, fkStr)
		if err != nil {
			// Infrastructure failure: the user cannot fix a DB / repository
			// error by editing record data. Wrap with ErrChainResolverFailure
			// so resolveSingleRef can keep this distinct from user-data
			// errors when classifying multi-chain outcomes.
			return nil, fmt.Errorf("%w: step %d FindOne(%s, %s): %v",
				ErrChainResolverFailure, i, lookupTable, fkStr, err)
		}
		if next == nil {
			// User-data condition: the FK pointed at a non-existent row
			// (typo / deleted parent). The user can fix this.
			return nil, fmt.Errorf("%w: step %d %s/%s not found",
				ErrTokenResolve, i, lookupTable, fkStr)
		}
		cur = next
	}
	return cur, nil
}
