package records

import (
	"database/sql"
	"fmt"
	"strings"

	"github.com/nexuscrm/kernel/pkg/apierr"
	appErrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// immutableObjectFields are fields on _System_Object that cannot be modified via generic CRUD.
// RequiredReadPermission is included because it is a runtime security-gate value: the
// permission engine reads it directly from the DB-loaded schema (not from system_tables.json),
// so a generic CRUD write can silently downgrade the audit-payload read gate without any
// assertion catching it. Most fields in this map are bypassed by ElevatedContext
// and SystemContext (BypassesCRUDandFLS); unbypassableObjectFields below are
// blocked for every generic-record writer. In current code setup-apply
// reconciliation writes these fields via raw SQL and bypasses PersistenceService
// entirely. See #503.
var immutableObjectFields = map[string]bool{
	"SharingModel": true, "EnableHierarchySharing": true,
	"IsSystem": true, "IsCustom": true, "ApiName": true,
	"TableType": true, "RequiredWritePermission": true,
	"RequiredReadPermission": true,
	// LockRule and LockRuleCustomized must go through MetadataService authoring
	// paths (CreateSchema / UpdateSchema) so ValidateLockRuleExpression runs and
	// the column-scoped ownership bit is set correctly. Generic CRUD writes would
	// bypass the validator and the LockRuleCustomized guard, breaking the pack
	// reapply protection. See PR #948.
	"LockRule": true, "LockRuleCustomized": true,
	// ReadSource is a behavior-affecting metadata authority declaration. Until
	// there is an explicit validated authoring surface, generic CRUD must not
	// persist it into live _System_Object rows.
	constants.FieldSysObject_ReadSource: true,
	// ExclusiveSiblingFields is executable metadata. It must enter through pack
	// apply or tenant overlay validation so malformed declarations and pack/L2
	// ownership bypasses cannot reach the records runtime through generic CRUD.
	constants.FieldSysObject_ExclusiveSiblingFields:                  true,
	constants.FieldSysObject_ExclusiveSiblingFieldsCustomized:        true,
	constants.FieldSysObject_ExclusiveSiblingFieldsPackName:          true,
	constants.FieldSysObject_ExclusiveSiblingFieldsPackBusinessKey:   true,
	constants.FieldSysObject_ExclusiveSiblingFieldsPackManageability: true,
	constants.FieldSysObject_ExclusiveSiblingFieldsPackValue:         true,
	// DisplayNameField must go through MetadataService.UpdateSchema so the
	// incoming value is validated against the live object schema (field existence
	// and renderable type). Generic CRUD writes would bypass that validation and
	// allow arbitrary values that break list-view queries.
	constants.FieldSysObject_DisplayNameField: true,
}

// unbypassableObjectFields are executable metadata declaration surfaces. They
// must not be authored through generic records CRUD, even by SystemContext flows
// or other trusted record writers; validated pack/tenant apply paths use
// metadata/raw persistence instead.
var unbypassableObjectFields = map[string]bool{
	constants.FieldSysObject_ExclusiveSiblingFields:                  true,
	constants.FieldSysObject_ExclusiveSiblingFieldsCustomized:        true,
	constants.FieldSysObject_ExclusiveSiblingFieldsPackName:          true,
	constants.FieldSysObject_ExclusiveSiblingFieldsPackBusinessKey:   true,
	constants.FieldSysObject_ExclusiveSiblingFieldsPackManageability: true,
	constants.FieldSysObject_ExclusiveSiblingFieldsPackValue:         true,
}

var unbypassableEmailTemplateFields = map[string]bool{
	constants.FieldSysEmailTemplate_OwnerPack: true,
}

var unbypassableListViewFields = map[string]bool{
	constants.FieldSysListView_OwnerID:       true,
	constants.FieldSysListView_Visibility:    true,
	constants.FieldSysListView_DeveloperName: true,
}

var unbypassableFieldFields = map[string]bool{
	constants.FieldSysField_ReferenceTo:    true,
	constants.FieldSysField_DeleteRule:     true,
	constants.FieldSysField_IsMasterDetail: true,
	constants.FieldSysField_IsPolymorphic:  true,
}

// immutableFieldFields are fields on _System_Field that cannot be modified via generic CRUD.
var immutableFieldFields = map[string]bool{
	"Createable": true, "Updateable": true, "IsSystem": true,
	"Type": true, "Formula": true, "ApiName": true,
	"OwnerKind": true, "OwnerPack": true,
	constants.FieldSysField_SiblingMaxParentField: true,
}

var immutableInstalledPackFields = map[string]bool{
	constants.FieldStatus: true,
}

// toolingOnlyTables lists metadata tables whose write contract lives in the
// Tooling API service layer, not generic /sobjects CRUD.
var toolingOnlyTables = map[string]bool{
	// Flow tooling endpoints (flow_tooling.go).
	"FlowDefinition": true,
	"Flow":           true,
	// CustomObject: full dedicated write surface exists, so blocking generic
	// /sobjects CRUD removes no capability — create via POST /metadata/objects
	// and POST /tooling/sobjects/CustomObject, update via PATCH
	// /tooling/sobjects/CustomObject/:id, delete via DELETE /metadata/objects/:apiName.
	"_System_Object": true,
	// CustomApplication: full dedicated Tooling CRUD exists — POST/PATCH/DELETE
	// /tooling/sobjects/CustomApplication[/:id].
	"_System_App": true,
	// Bulk ingest control rows are owned by the Bulk API handlers/contracts.
	// Generic CRUD must not mint jobs or batches that bypass create/upload/close
	// validation and worker provenance assumptions.
	constants.TableBulkJob:   true,
	constants.TableBulkBatch: true,
	// NOTE: only tables with a COMPLETE replacement write surface are gated
	// here. The following are deliberately NOT included because gating them
	// would remove the only write path (no equivalent Tooling/Admin route for
	// one or more of create/update/delete), which would regress admin/API
	// self-service:
	//   _System_Field        (no update route outside generic CRUD)
	//   ProfileLayout        (create-only tooling route)
	//   _System_ApprovalProcess, _System_RecordType, _System_Webhook
	//                        (no dedicated tooling/admin write route)
	//   _System_Permission, _System_ReportType
	//                        (row-driven writable metadata; no replacement route)
	//   _System_ValidationRule (needs a Salesforce-Tooling-compliant read+write
	//                        surface first — tracked in #1725)
	//   _System_ListView, _System_EmailTemplate, _System_ConnectedApp,
	//   _System_StandardAction (prior review BLOCK: no equivalent write surface)
	// Add each here once a compliant replacement write surface lands.
	//
	// _System_ListView bulk/collection/tree surfaces are guarded separately by
	// rejectGenericListViewMutation with a neutral invalid-operation error.
	// Single-record generic CRUD/upsert remains the legacy/global maintenance
	// path until a complete replacement write surface exists.
}

func rejectToolingOnlyTableWrites(objectName string) error {
	if toolingOnlyTables[objectName] {
		return appErrors.NewFLSDeniedErrorForFields([]string{objectName})
	}
	for k := range toolingOnlyTables {
		if strings.EqualFold(objectName, k) {
			return appErrors.NewFLSDeniedErrorForFields([]string{objectName})
		}
	}
	return nil
}

func isListViewObject(objectName string) bool {
	return strings.EqualFold(strings.TrimSpace(objectName), constants.TableListView)
}

func rejectGenericListViewMutation(objectName, operation string) error {
	if !isListViewObject(objectName) {
		return nil
	}
	operation = strings.TrimSpace(operation)
	if operation == "" {
		operation = "mutation"
	}
	return appErrors.NewInvalidOperationError(fmt.Sprintf("%s is not writable through generic %s surfaces", constants.TableListView, operation))
}

// rejectImmutableMetadataFieldWrites blocks writes to immutable metadata fields on
// _System_Object, _System_Field, and selected control-plane metadata tables via generic CRUD. Schema changes must go
// through the Metadata API which enforces additional consistency checks.
// Gated at `BypassesCRUDandFLS` (ElevatedContext and above bypass) because
// metadata-management services legitimately need to edit these tables.
func rejectImmutableMetadataFieldWrites(objectName string, data models.SObject) error {
	var immutable map[string]bool
	switch {
	case strings.EqualFold(objectName, constants.TableObject):
		immutable = immutableObjectFields
	case strings.EqualFold(objectName, constants.TableField):
		immutable = immutableFieldFields
	case strings.EqualFold(objectName, "_System_InstalledPack"):
		immutable = immutableInstalledPackFields
	default:
		return nil
	}
	return rejectMetadataFieldWrites(data, immutable)
}

func rejectUnbypassableMetadataFieldWrites(objectName string, data models.SObject) error {
	switch {
	case strings.EqualFold(objectName, constants.TableObject):
		return rejectMetadataFieldWrites(data, unbypassableObjectFields)
	case strings.EqualFold(objectName, constants.TableField):
		return rejectMetadataFieldWrites(data, unbypassableFieldFields)
	case strings.EqualFold(objectName, constants.TableEmailTemplate):
		return rejectMetadataFieldWrites(data, unbypassableEmailTemplateFields)
	case strings.EqualFold(objectName, constants.TableListView):
		return rejectMetadataFieldWrites(data, unbypassableListViewFields)
	default:
		return nil
	}
}

func rejectManagedEmailTemplateGenericMutation(objectName string, record models.SObject, operation string) error {
	if !strings.EqualFold(objectName, constants.TableEmailTemplate) || record == nil {
		return nil
	}
	ownerPack := emailTemplateOwnerPackValue(record)
	if ownerPack == "" {
		return nil
	}
	id, _ := record[constants.FieldID].(string)
	if id == "" {
		id = "(unknown id)"
	}
	return apierr.Forbidden(fmt.Sprintf("%s row %s is owned by pack %q and cannot be %s through generic CRUD", constants.TableEmailTemplate, id, ownerPack, operation))
}

func emailTemplateOwnerPackValue(record models.SObject) string {
	if record == nil {
		return ""
	}
	switch v := record[constants.FieldSysEmailTemplate_OwnerPack].(type) {
	case string:
		return strings.TrimSpace(v)
	case []byte:
		return strings.TrimSpace(string(v))
	case sql.NullString:
		if v.Valid {
			return strings.TrimSpace(v.String)
		}
	}
	return ""
}

func rejectMetadataFieldWrites(data models.SObject, immutable map[string]bool) error {
	// Collect ALL immutable fields touched — matches SF v63.0 which lists every
	// non-writable field in one error per record.
	var denied []string
	for field := range data {
		if immutable[field] {
			denied = append(denied, field)
		}
	}
	if len(denied) > 0 {
		// SF v63.0 emits INVALID_FIELD_FOR_INSERT_UPDATE for any non-writable
		// field rejection (verified against sandbox for CreatedDate). The
		// handler layer unwraps FLSDeniedError into the SF shape.
		return appErrors.NewFLSDeniedErrorForFields(denied)
	}
	return nil
}

// rejectProtectedGuardFieldWrites blocks create/update writes to any field
// declared as the `protectedRecords.guardField` of its table in
// system_tables.json. This is the natural complement to the delete guard —
// the `reject_delete` contract cannot hold if the guard field itself can be
// flipped by non-SystemContext callers, nor if a user can INSERT a row with
// the guard pre-set to true to fabricate an undeletable record.
//
// Gated at the **same `< SystemContext` threshold as the delete guard** so
// the two invariants are consistent. Only bootstrap and data-specs (which
// run at SystemContext) can write to a guard field. ElevatedContext is NOT
// allowed — elevated real-user paths must not be able to flip protection.
//
// Covers both Update and Insert (single + bulk) paths.
func rejectProtectedGuardFieldWrites(objectName string, data models.SObject) error {
	spec, ok := lookupProtectedSpec(objectName)
	if !ok {
		return nil
	}
	for field := range data {
		if strings.EqualFold(field, spec.GuardField) {
			return appErrors.NewInvalidFieldForWriteError([]string{field},
				fmt.Sprintf("cannot be set via generic CRUD on %s; field is the platform-managed guard for %s",
					objectName, spec.ErrorCode))
		}
	}
	return nil
}
