package tenantoverlay

import (
	"bytes"
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"log"
	"path/filepath"
	"sort"
	"strings"

	"github.com/nexuscrm/backend/internal/bootstrap"
	domainschema "github.com/nexuscrm/backend/internal/domain/schema"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// applyInsertRules seeds tenant-authored insert-inheritance + child-clone
// metadata surfaces:
//   - _System_ObjectFieldMapping rows with ActionType='insertInheritFromLookup'
//     (from object_field_mappings.json)
//   - _System_ObjectFieldMapping rows with other seed-authored ActionType values
//   - _System_Object.ChildCloneRules JSON column (from object_child_clone_rules.json)
//
// Idempotent: repeated apply-tenant runs produce no drift; fields stamped
// by previous applies are overwritten only when the file changes.
func (p *Pack) applyInsertRules(ctx context.Context, db *sql.DB) error {
	if err := p.applyObjectFieldMappingsOverlay(ctx, db); err != nil {
		return fmt.Errorf("tenant object field mappings: %w", err)
	}
	if err := p.applyObjectChildCloneRulesOverlay(ctx, db); err != nil {
		return fmt.Errorf("tenant object child clone rules: %w", err)
	}
	return nil
}

// applyObjectFieldMappingsOverlay routes tenant-seeded
// object-field-mapping rows through the shared owner-scoped writer. The
// tenant owner is part of the reconcile WHERE clause, so a partial overlay
// can add rows without pruning platform-standard mappings.
func (p *Pack) applyObjectFieldMappingsOverlay(ctx context.Context, db *sql.DB) error {
	objectFieldMappingsJSON, ok, err := p.readOptional(ObjectFieldMappingsFile)
	if err != nil {
		return err
	}
	if !ok {
		return nil
	}
	var groups []bootstrap.FieldMappingGroup
	if err := json.Unmarshal(objectFieldMappingsJSON, &groups); err != nil {
		return fmt.Errorf("parse tenant object_field_mappings.json: %w", err)
	}
	if len(groups) == 0 {
		return nil
	}
	return bootstrap.UpsertFieldMappingGroupsForOwner(ctx, db, groups, p.objectFieldMappingOwner())
}

func (p *Pack) objectFieldMappingOwner() string {
	if p == nil || strings.TrimSpace(p.Dir) == "" {
		return "tenant:overlay"
	}
	base := filepath.Base(filepath.Clean(p.Dir))
	if base == "." || base == string(filepath.Separator) || strings.TrimSpace(base) == "" {
		return "tenant:overlay"
	}
	return "tenant:" + base
}

// applyObjectChildCloneRulesOverlay UPDATEs `_System_Object.ChildCloneRules`
// per object key in the seed JSON. Rows are matched by the deterministic
// object Id (domainschema.GenerateObjectID(apiName)).
//
// Authority model — tenant-overlay-owned, NOT admin-editable (round-7
// reviewer finding #2, upper half):
//
//   - This writer is an unconditional overwrite. Every apply-tenant run
//     replaces the entire ChildCloneRules JSON column with the seed
//     bytes. That is the deliberate contract, not a bug.
//   - Unlike _System_ObjectFieldMapping — where each row is admin-
//     editable with an IsCustomized=1 flag and the convergent upsert
//     respects that flag — ChildCloneRules lives as a single JSON blob
//     on _System_Object. There is no per-rule customization column, no
//     per-rule IsCustomized marker, and no runtime code that respects
//     admin edits to this column (child_clone.go reads the column
//     directly).
//   - Consequence: an admin cannot safely customize
//     _System_Object.ChildCloneRules via API/UI today. Any edit they
//     make will be overwritten on the next apply-tenant.
//   - Per-rule admin customization is a separate RFC (column-level
//     IsCustomized + runtime-aware reader). Scoping it into this
//     cutover would balloon PR size and touch runtime reads; the
//     honest choice is to declare this surface overlay-owned now and
//     add the customization mechanism in a follow-up if/when admin
//     authoring becomes a real requirement.
//
// Fail-closed on the ordering invariant:
//
//   - RowsAffected()==0 — hard error. The tenant-objects spec apply step
//     runs before this in pack.Apply and is responsible for ensuring the
//     parent object row exists. A zero-row match means apply-tenant's
//     ordering invariant is broken or someone renamed/removed the object
//     between steps. Either way, proceeding would leave the new
//     ChildCloneRules authority silently unlanded — the exact failure
//     mode round-3 reviewer finding #2 flagged.
//
//   - len(rules)==0 — explicit clear. Encoded as the empty JSON array
//     "[]" and written through, so a seed that deliberately removes all
//     rules for an object converges the DB column instead of leaving the
//     stale rules in place. "Skip on empty" would be an unclosed
//     cutover (Class 2 in the round-3 taxonomy).
func (p *Pack) applyObjectChildCloneRulesOverlay(ctx context.Context, db *sql.DB) error {
	objectChildCloneRulesJSON, ok, err := p.readOptional(ObjectChildCloneRulesFile)
	if err != nil {
		return err
	}
	if !ok {
		return nil
	}
	rulesByObject := map[string][]models.ChildCloneRule{}
	if err := json.Unmarshal(objectChildCloneRulesJSON, &rulesByObject); err != nil {
		return fmt.Errorf("parse tenant object_child_clone_rules.json: %w", err)
	}
	if len(rulesByObject) == 0 {
		return nil
	}

	for apiName, rules := range rulesByObject {
		objectID := domainschema.GenerateObjectID(apiName)
		// Nil-rules → empty array, NOT null. A null value in the column
		// is ambiguous with "never seeded"; "[]" says "authoritatively
		// empty after this apply" and the closure validator can
		// round-trip it identically.
		if rules == nil {
			rules = []models.ChildCloneRule{}
		}
		encoded, err := json.Marshal(rules)
		if err != nil {
			return fmt.Errorf("encode ChildCloneRules for %s: %w", apiName, err)
		}
		query := fmt.Sprintf(
			"UPDATE `%s` SET `%s` = ?, `%s` = NOW() WHERE `%s` = ?",
			constants.TableObject,
			constants.FieldSysObject_ChildCloneRules,
			constants.FieldLastModifiedDate,
			constants.ColID,
		)
		res, err := db.ExecContext(ctx, query, string(encoded), objectID)
		if err != nil {
			return fmt.Errorf("update ChildCloneRules for %s: %w", apiName, err)
		}
		affected, _ := res.RowsAffected()
		if affected == 0 {
			return fmt.Errorf(
				"ChildCloneRules overlay for %q updated 0 rows (expected id=%s on %s) — "+
					"parent object row missing. Check apply-tenant ordering: "+
					"tenant_objects_spec.json apply must precede applyInsertRules.",
				apiName, objectID, constants.TableObject,
			)
		}
		log.Printf("   ✅ ChildCloneRules seeded on %s (%d rules)", apiName, len(rules))
	}
	return nil
}

// objectAPINamesWithChildCloneRules returns the list of SObject API names
// the tenant seed declares ChildCloneRules for. Consumed by the tenant
// Verify path to cross-check that every declared target is a known
// metadata SObject — a cheap sanity guard before the heavier platform-layer
// validators run. Also invoked by the parity e2e-api fixture setup.
func (p *Pack) objectAPINamesWithChildCloneRules() ([]string, error) {
	objectChildCloneRulesJSON, ok, err := p.readOptional(ObjectChildCloneRulesFile)
	if err != nil {
		return nil, err
	}
	if !ok {
		return nil, nil
	}
	rulesByObject := map[string][]models.ChildCloneRule{}
	if err := json.Unmarshal(objectChildCloneRulesJSON, &rulesByObject); err != nil {
		return nil, fmt.Errorf("parse tenant object_child_clone_rules.json: %w", err)
	}
	out := make([]string, 0, len(rulesByObject))
	for k := range rulesByObject {
		out = append(out, k)
	}
	return out, nil
}

// assertTenantInsertRulesClean runs the pure-schema sanity pass after
// apply: every referenced object / field must resolve. This is a lighter
// check than the platform-level ValidateObjectChildCloneRules +
// ValidateInsertInheritFromLookupRules functions — those need a live
// metadata service, which this tenant overlay does have via ApplyDeps,
// so the two callers can coexist.
func (p *Pack) assertTenantInsertRulesSurfacesLoadable() error {
	// Parse both files purely to catch JSON schema drift at apply time
	// (e.g. someone renamed a field). Detailed cross-schema checks run
	// in the platform validator pass immediately after.
	if objectFieldMappingsJSON, ok, err := p.readOptional(ObjectFieldMappingsFile); err != nil {
		return err
	} else if ok {
		var groups []bootstrap.FieldMappingGroup
		if err := json.Unmarshal(objectFieldMappingsJSON, &groups); err != nil {
			return fmt.Errorf("tenant object_field_mappings.json is not parseable as FieldMappingGroup: %w", err)
		}
		for _, g := range groups {
			if strings.TrimSpace(g.SourceObject) == "" || strings.TrimSpace(g.TargetObject) == "" {
				return fmt.Errorf("tenant object_field_mappings.json group missing sourceObject/targetObject: %+v", g)
			}
			if g.ActionType == "" {
				return fmt.Errorf("tenant object_field_mappings.json group missing actionType: %s→%s", g.SourceObject, g.TargetObject)
			}
		}
	}
	if objectChildCloneRulesJSON, ok, err := p.readOptional(ObjectChildCloneRulesFile); err != nil {
		return err
	} else if ok {
		rules := map[string][]models.ChildCloneRule{}
		if err := json.Unmarshal(objectChildCloneRulesJSON, &rules); err != nil {
			return fmt.Errorf("tenant object_child_clone_rules.json is not parseable: %w", err)
		}
	}
	return nil
}

// VerifyInsertRulesLanded is the declared-vs-DB closure check for the two
// tenant metadata surfaces introduced by the insert-inheritance cutover.
// Invoked only from controlplane.ApplyTenant (NOT from generic
// ApplyPacks — platform boundary, round-7 reviewer finding #1).
//
// Why this exists separately from ValidateObjectChildCloneRules /
// ValidateInsertInheritFromLookupRules (packs/insert_rules_validator.go):
// those validators inspect the rows that *currently exist* in DB and
// reject any that reference missing metadata. They cannot detect a
// silent write-miss ("seed declared X, DB has no X") because
// "no row in DB" is indistinguishable from "seed declared nothing." The
// round-3 reviewer finding #2 is exactly this blind spot.
//
// The two surfaces have DIFFERENT authority models, so the closure
// applies DIFFERENT verification strictness to each
// (round-7 reviewer finding #2):
//
//   - _System_ObjectFieldMapping is admin-editable metadata. DB is the
//     live authority; seed is a default template; IsCustomized=1 rows
//     are first-class admin overrides preserved by the convergent
//     upsert. Closure here is BUSINESS-KEY EXISTENCE only — it asserts
//     every seed-declared tenant-owned (SourceObject, TargetObject,
//     ActionType, SourceField) has an active, non-deleted row in DB.
//     Values (TargetField / DefaultValue / SourceLookupField) are NOT
//     compared — admin edits must survive. See
//     verifyObjectFieldMappingsLanded.
//
//   - _System_Object.ChildCloneRules is tenant-overlay-owned, not
//     admin-editable. The writer is unconditional overwrite; runtime
//     reads the column directly with no per-rule IsCustomized support.
//     Byte-equal canonical comparison is therefore the correct closure
//     for this surface — there is no legitimate admin divergence to
//     protect. See verifyChildCloneRulesLanded.
//
// If a future PR adds per-rule admin customization for ChildCloneRules
// (column-level IsCustomized + runtime-aware reader), the
// verifyChildCloneRulesLanded closure must weaken to existence-only at
// that point — but not before, because weakening now would hide real
// write-miss bugs under the "admin might have edited it" excuse.
func (p *Pack) VerifyInsertRulesLanded(ctx context.Context, db *sql.DB) error {
	if db == nil {
		return nil
	}
	if err := p.verifyChildCloneRulesLanded(ctx, db); err != nil {
		return err
	}
	if err := p.verifyObjectFieldMappingsLanded(ctx, db); err != nil {
		return err
	}
	return nil
}

// verifyChildCloneRulesLanded reads the seed map and asserts that for
// every declared apiName, the corresponding _System_Object row's
// ChildCloneRules column canonically equals the seed. Missing object
// rows, empty columns, or divergent rule sets all fail with a named
// object + the canonical diff.
//
// Byte-equal is the correct closure strictness here because this
// surface is tenant-overlay-owned (see VerifyInsertRulesLanded's
// authority-model note, and applyObjectChildCloneRulesOverlay's
// unconditional-overwrite contract). Divergence between seed and DB
// is a bug — either a write-miss or an unauthorized direct column
// edit — not an admin override to be respected.
func (p *Pack) verifyChildCloneRulesLanded(ctx context.Context, db *sql.DB) error {
	objectChildCloneRulesJSON, ok, err := p.readOptional(ObjectChildCloneRulesFile)
	if err != nil {
		return err
	}
	if !ok {
		return nil
	}
	seed := map[string][]models.ChildCloneRule{}
	if err := json.Unmarshal(objectChildCloneRulesJSON, &seed); err != nil {
		return fmt.Errorf("closure: parse object_child_clone_rules.json: %w", err)
	}

	for apiName, seedRules := range seed {
		objectID := domainschema.GenerateObjectID(apiName)
		query := fmt.Sprintf(
			"SELECT `%s` FROM `%s` WHERE `%s` = ?",
			constants.FieldSysObject_ChildCloneRules,
			constants.TableObject,
			constants.ColID,
		)
		var stored sql.NullString
		row := db.QueryRowContext(ctx, query, objectID)
		if err := row.Scan(&stored); err != nil {
			if err == sql.ErrNoRows {
				return fmt.Errorf(
					"ChildCloneRules closure failed: object %q (id=%s) has no row in %s — "+
						"apply-tenant ordering is broken or the object was renamed",
					apiName, objectID, constants.TableObject,
				)
			}
			return fmt.Errorf("closure: read ChildCloneRules for %s: %w", apiName, err)
		}

		var dbRules []models.ChildCloneRule
		if stored.Valid && strings.TrimSpace(stored.String) != "" {
			if err := json.Unmarshal([]byte(stored.String), &dbRules); err != nil {
				return fmt.Errorf("closure: DB ChildCloneRules for %s is not valid JSON: %w", apiName, err)
			}
		}
		if dbRules == nil {
			dbRules = []models.ChildCloneRule{}
		}
		if seedRules == nil {
			seedRules = []models.ChildCloneRule{}
		}

		seedBytes, err := canonicalChildCloneRules(seedRules)
		if err != nil {
			return fmt.Errorf("closure: canonicalize seed ChildCloneRules for %s: %w", apiName, err)
		}
		dbBytes, err := canonicalChildCloneRules(dbRules)
		if err != nil {
			return fmt.Errorf("closure: canonicalize DB ChildCloneRules for %s: %w", apiName, err)
		}
		if !bytes.Equal(seedBytes, dbBytes) {
			return fmt.Errorf(
				"ChildCloneRules closure failed on %s: seed declared %d rule(s), DB has %d rule(s) — "+
					"reapply with fail-closed writer required.\n"+
					"  SEED (canonical): %s\n"+
					"  DB   (canonical): %s",
				apiName, len(seedRules), len(dbRules), string(seedBytes), string(dbBytes),
			)
		}
	}
	return nil
}

// verifyObjectFieldMappingsLanded asserts, for every row declared in
// the seed, that an authoritative row exists in DB with the matching
// BUSINESS UNIQUE KEY — (Source, SourceObject, TargetObject, ActionType,
// SourceField). Nothing else is compared.
//
// Why business-key existence, NOT value equality (round-7 reviewer
// finding #2 correction):
//
//  1. _System_ObjectFieldMapping is admin-editable metadata. An admin
//     can edit TargetField / DefaultValue / SourceLookupField and stamp
//     IsCustomized=1; the convergent upsert (field_mappings.go ON
//     DUPLICATE KEY UPDATE) deliberately preserves those edits. If
//     closure compared values, the admin override would trigger a
//     closure failure on the next apply-tenant — breaking the
//     admin-edits-are-first-class contract of
//     _System_ObjectFieldMapping.
//
//  2. The generated Id encodes Source owner, SourceField, and TargetField,
//     but the unique key is only (Source, SourceObject, TargetObject,
//     ActionType, SourceField). When the seed changes TargetField for an
//     existing owner-owned row, ON DUPLICATE KEY UPDATE collides on the
//     unique key and updates TargetField on the old primary-key row; the
//     row's Id does NOT change (primary keys are immutable). A closure that
//     looked up by seed-derived Id would miss the row and report a false
//     negative.
//
// Closure's job reduces to: "did every seed-declared tenant-owned
// business key land in DB as an active, non-deleted row?" That's
// sufficient to catch silent write-miss (RowsAffected==0 on the upsert)
// and owner-stamping regressions, and non-fragile under admin overrides.
//
// Soft-deleted (IsDeleted=1) and inactive (IsActive=0) rows are
// excluded — those are tombstones / disabled rows and do not count
// as "landed".
func (p *Pack) verifyObjectFieldMappingsLanded(ctx context.Context, db *sql.DB) error {
	objectFieldMappingsJSON, ok, err := p.readOptional(ObjectFieldMappingsFile)
	if err != nil {
		return err
	}
	if !ok {
		return nil
	}
	var groups []bootstrap.FieldMappingGroup
	if err := json.Unmarshal(objectFieldMappingsJSON, &groups); err != nil {
		return fmt.Errorf("closure: parse object_field_mappings.json: %w", err)
	}

	const existenceQuery = `
		SELECT 1 FROM _System_ObjectFieldMapping
		WHERE SourceObject = ? AND TargetObject = ? AND ActionType = ?
		  AND SourceField = ? AND Source = ? AND IsActive = 1 AND IsDeleted = 0
		LIMIT 1`

	for _, g := range groups {
		for _, m := range g.Mappings {
			var found int
			err := db.QueryRowContext(ctx, existenceQuery,
				g.SourceObject, g.TargetObject, g.ActionType, m.SourceField, p.objectFieldMappingOwner(),
			).Scan(&found)
			if err == sql.ErrNoRows {
				return fmt.Errorf(
					"ObjectFieldMapping closure failed on %s→%s(%s) SourceField=%q: "+
						"no active, non-deleted tenant-owned row for this business unique key. "+
						"Either the convergent upsert failed silently on a previous apply, "+
						"or a rogue delete left the surface incomplete. Reapply required.",
					g.SourceObject, g.TargetObject, g.ActionType, m.SourceField,
				)
			}
			if err != nil {
				return fmt.Errorf("closure: query mapping %s→%s(%s) SourceField=%q: %w",
					g.SourceObject, g.TargetObject, g.ActionType, m.SourceField, err)
			}
		}
	}
	return nil
}

// canonicalChildCloneRules produces a deterministic byte form for deep
// comparison. Go's json.Marshal sorts map keys alphabetically, so the
// only non-determinism left is the rule list order. We key each rule by
// (sourceObject, targetObject, sourceLookupField) — the triple that
// uniquely identifies a ChildCloneRule within an object's slice — and
// sort by that composite key before marshalling.
func canonicalChildCloneRules(rules []models.ChildCloneRule) ([]byte, error) {
	sorted := make([]models.ChildCloneRule, len(rules))
	copy(sorted, rules)
	sort.SliceStable(sorted, func(i, j int) bool {
		a := sorted[i].SourceObject + "\x00" + sorted[i].TargetObject + "\x00" + sorted[i].SourceLookupField + "\x00" + sorted[i].ParentLookupField
		b := sorted[j].SourceObject + "\x00" + sorted[j].TargetObject + "\x00" + sorted[j].SourceLookupField + "\x00" + sorted[j].ParentLookupField
		return a < b
	})
	return json.Marshal(sorted)
}
