package schema

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

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

// AccessPolicySidecarSnapshot carries the live DB projection for AccessPolicy
// subfacts that may be owned independently from the parent object metadata.
type AccessPolicySidecarSnapshot struct {
	LiveRaw                 sql.NullString
	RecordMutationPackName  sql.NullString
	RecordMutationPackValue sql.NullString
	LifecyclePackName       sql.NullString
	LifecyclePackValue      sql.NullString
}

type AccessPolicySidecarPreserveOptions struct {
	ObjectAPIName          string
	RequireOwnedProjection bool
}

func PreservePackOwnedAccessPolicySidecars(
	policy *models.ObjectAccessPolicy,
	snapshot AccessPolicySidecarSnapshot,
	options AccessPolicySidecarPreserveOptions,
) (*models.ObjectAccessPolicy, error) {
	preserveRecordMutation := strings.TrimSpace(snapshot.RecordMutationPackName.String) != ""
	preserveLifecycle := strings.TrimSpace(snapshot.LifecyclePackName.String) != ""
	if !preserveRecordMutation && !preserveLifecycle {
		return policy, nil
	}

	livePolicy, err := DecodeLiveObjectAccessPolicyForSidecarPreservation(snapshot.LiveRaw, options.ObjectAPIName)
	if err != nil {
		return nil, err
	}
	if policy == nil {
		policy = &models.ObjectAccessPolicy{}
	}

	if preserveRecordMutation {
		recordMutation := livePolicy.RecordMutation
		if recordMutation == nil {
			recordMutation, err = DecodeRecordMutationAccessPolicyPackValue(snapshot.RecordMutationPackValue, options.ObjectAPIName)
			if err != nil {
				return nil, err
			}
		}
		if recordMutation == nil && options.RequireOwnedProjection {
			return nil, fmt.Errorf("%s AccessPolicy.recordMutation is owned by pack %s but live projection and sidecar value are both empty", options.ObjectAPIName, strings.TrimSpace(snapshot.RecordMutationPackName.String))
		}
		policy.RecordMutation = recordMutation
	}

	if preserveLifecycle {
		lifecycle := livePolicy.Lifecycle
		if lifecycle == nil {
			lifecycle, err = DecodeLifecycleAccessPolicyPackValue(snapshot.LifecyclePackValue, options.ObjectAPIName)
			if err != nil {
				return nil, err
			}
		}
		if lifecycle == nil && options.RequireOwnedProjection {
			return nil, fmt.Errorf("%s AccessPolicy.lifecycle is owned by pack %s but live projection and sidecar value are both empty", options.ObjectAPIName, strings.TrimSpace(snapshot.LifecyclePackName.String))
		}
		policy.Lifecycle = lifecycle
	}

	if ObjectAccessPolicyIsEmpty(policy) {
		return nil, nil
	}
	return policy, nil
}

func DecodeLiveObjectAccessPolicyForSidecarPreservation(raw sql.NullString, objectAPIName string) (*models.ObjectAccessPolicy, error) {
	if !raw.Valid || strings.TrimSpace(raw.String) == "" {
		return &models.ObjectAccessPolicy{}, nil
	}
	var policy models.ObjectAccessPolicy
	if err := json.Unmarshal([]byte(raw.String), &policy); err != nil {
		return nil, fmt.Errorf("%s live AccessPolicy is not valid JSON while preserving pack-owned AccessPolicy sidecars: %w", objectAPIName, err)
	}
	return &policy, nil
}

func DecodeRecordMutationAccessPolicyPackValue(raw sql.NullString, objectAPIName string) (*models.RecordMutationAccessPolicy, error) {
	value := strings.TrimSpace(raw.String)
	if !raw.Valid || value == "" || value == "null" {
		return nil, nil
	}
	var policy models.RecordMutationAccessPolicy
	if err := json.Unmarshal([]byte(value), &policy); err != nil {
		return nil, fmt.Errorf("%s AccessPolicyRecordMutationPackValue is not valid JSON: %w", objectAPIName, err)
	}
	return &policy, nil
}

func DecodeLifecycleAccessPolicyPackValue(raw sql.NullString, objectAPIName string) (*models.RecordLifecycleAccessPolicy, error) {
	value := strings.TrimSpace(raw.String)
	if !raw.Valid || value == "" || value == "null" {
		return nil, nil
	}
	var policy models.RecordLifecycleAccessPolicy
	if err := json.Unmarshal([]byte(value), &policy); err != nil {
		return nil, fmt.Errorf("%s AccessPolicyLifecyclePackValue is not valid JSON: %w", objectAPIName, err)
	}
	return &policy, nil
}

func ObjectAccessPolicyIsEmpty(policy *models.ObjectAccessPolicy) bool {
	return policy == nil ||
		(strings.TrimSpace(policy.Mode) == "" &&
			policy.ObjectRead == nil &&
			policy.ObjectCreate == nil &&
			policy.ObjectEdit == nil &&
			policy.ObjectDelete == nil &&
			policy.FieldRead == nil &&
			policy.FieldEdit == nil &&
			policy.RowRead == nil &&
			policy.RecordMutation == nil &&
			policy.Lifecycle == nil &&
			policy.CacheInvalidation == nil &&
			policy.WriteValidation == nil)
}
