package controlplane

import (
	"context"
	"fmt"
	"log"
	"os"
	"strings"
	"time"

	"github.com/pingcap-inc/nexuscrm-controlplane/packs"
	"github.com/pingcap-inc/nexuscrm-controlplane/tenantoverlay"
)

const TenantOverlayDirEnv = "NEXUSCRM_TENANT_OVERLAY_DIR"
const TenantOverlayRepairLegacyUniqueDuplicatesEnv = "NEXUSCRM_TENANT_OVERLAY_REPAIR_LEGACY_UNIQUE_DUPLICATES"

// ApplyTenant layers tenant-specific metadata on top of an already-
// applied platform baseline. It is the entry point for
// `cmd/setup apply-tenant` — deployment tooling may invoke it from
// infra/entrypoint.sh apply modes only on deployments that opt in via
// NEXUSCRM_APPLY_TENANT_OVERLAY=1. Greenfield orgs never call this.
//
// Apply is an idempotent reconcile to the selected overlay's desired state, so
// it is safe to re-run on every boot that opts into overlay mutation
// (NEXUSCRM_APPLY_TENANT_OVERLAY=1). There is intentionally no skip-on-marker
// fast path: unlike the baseline control plane — whose restart skip is made
// fail-closed by the rich, complete controlplane.Verify readiness gate — the
// tenant overlay has no complete readiness backstop (readiness verify is
// tenant-agnostic and the overlay's own Verify checks only object/field shape,
// not record types, auto-number, action/related-list overlays, insert/child-clone
// rules, object SharingModel, or tenant flows). Re-applying converges every
// overlay surface and heals drift rather than risking a silent serve of
// stale/partial tenant metadata. A pure no-mutation restart leaves the opt-in
// flag unset (baseline still starts start-only). Giving the overlay a total
// readiness verify — which would let it become skip-on-restart like baseline —
// is tracked as follow-up.
//
// Set NEXUSCRM_TENANT_OVERLAY_REPAIR_LEGACY_UNIQUE_DUPLICATES=1 to let this
// supported apply-tenant entrypoint clear repairable legacy duplicate values
// before unique-index DDL.
func ApplyTenant(ctx context.Context, r *Runner) error {
	return ApplyTenantWithOptions(ctx, r, applyTenantOptionsFromEnv())
}

func ApplyTenantWithOptions(ctx context.Context, r *Runner, options packs.ApplyOptions) error {
	// Deliberately use the apply lock, but do not invalidate the baseline
	// control_plane_apply_all marker here. That marker proves only baseline
	// ApplyAll convergence; tenant overlay convergence has a separate contract:
	// every boot that opts into tenant mutation re-applies the overlay, and a
	// failed overlay apply prevents server start. A baseline ApplyAll cannot
	// repair tenant overlay drift, so downgrading the baseline marker would only
	// force unrelated baseline work without closing the tenant convergence gap.
	return WithControlPlaneApplyLock(ctx, r.DB.DB(), "ApplyTenant", func() error {
		overlayDir := strings.TrimSpace(os.Getenv(TenantOverlayDirEnv))
		if overlayDir == "" {
			return fmt.Errorf("%s must point at the tenant overlay directory", TenantOverlayDirEnv)
		}
		return applyTenantUnlocked(ctx, r, overlayDir, options)
	})
}

func applyTenantOptionsFromEnv() packs.ApplyOptions {
	return packs.ApplyOptions{
		RepairLegacyUniqueDuplicates: tenantOverlayRepairLegacyUniqueDuplicatesRequested(),
	}
}

func tenantOverlayRepairLegacyUniqueDuplicatesRequested() bool {
	value := strings.TrimSpace(os.Getenv(TenantOverlayRepairLegacyUniqueDuplicatesEnv))
	return value == "1" || strings.EqualFold(value, "true")
}

func applyTenantUnlocked(ctx context.Context, r *Runner, overlayDir string, options packs.ApplyOptions) error {
	log.Println("═══ ApplyTenant: tenant overlay ═══")
	start := time.Now()

	pack := tenantoverlay.New(overlayDir)
	if err := pack.RegisterFieldManifest(); err != nil {
		return fmt.Errorf("tenant field manifest: %w", err)
	}
	if err := pack.RegisterRelatedListRules(); err != nil {
		return fmt.Errorf("tenant related-list rules: %w", err)
	}
	if err := fieldProvenanceInitializer.InitFieldProvenance(fieldProvenanceInitializer.DefaultPlatformStandardFields()); err != nil {
		return fmt.Errorf("tenant field provenance: %w", err)
	}
	if err := pack.Apply(ctx, r.PackDepsWithOptions(options)); err != nil {
		return fmt.Errorf("tenant overlay apply: %w", err)
	}

	if err := r.RefreshCache(ctx); err != nil {
		return fmt.Errorf("tenant post-apply cache refresh: %w", err)
	}

	// Cross-schema validators for DB-backed metadata surfaces.
	// Runs AFTER RefreshCache so validators see authoritative
	// post-apply state (not a stale cache). A bad tenant rule aborts
	// apply-tenant; the operator fixes the JSON and retries.
	if err := runInsertRulesValidators(ctx, r); err != nil {
		return fmt.Errorf("tenant insert-rules validation: %w", err)
	}

	// Declared-vs-DB closure for tenant object-field-mapping seed rows.
	// Lives HERE (not in runInsertRulesValidators) because its
	// inputs are the selected tenant overlay files —
	// platform boundary: the generic ApplyPacks path must stay
	// tenant-agnostic. Round-7 reviewer finding #1: putting this
	// in a shared validator function leaked tenant-specific reads
	// into greenfield `cmd/setup apply`, which then failed because
	// DB couldn't match tenant overlay seeds that were never
	// applied. This call is reachable only via ApplyTenant, so
	// greenfield never hits it.
	if err := pack.VerifyInsertRulesLanded(ctx, r.DB.DB()); err != nil {
		return fmt.Errorf("tenant insert-rules closure: %w", err)
	}

	// Symmetric to ApplyAll's step-7 verify: the tenant overlay
	// owns its own drift check, so greenfield boots (which never
	// call ApplyTenant) never hit it. strictMissing=true here is
	// correct — on a tenant-enabled deployment every spec-declared
	// field must have landed.
	if err := pack.Verify(ctx, packs.VerifyDeps{DB: r.DB.DB(), DeepChecks: false}); err != nil {
		return fmt.Errorf("tenant overlay verify: %w", err)
	}

	log.Printf("═══ ApplyTenant completed (%.1fs) ═══", time.Since(start).Seconds())
	return nil
}
