package packs

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

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

type sourceBackedCustomFieldPayload struct {
	ObjectAPIName string `json:"objectApiName"`
	FieldAPIName  string `json:"fieldApiName"`
}

type sourceBackedCustomFieldComponent struct {
	packName          string
	manifestComponent ManifestComponent
	payload           []byte
	declared          declaredCustomFieldExtension
}

func newSourceBackedCustomFieldComponent(packName string, manifestComponent ManifestComponent, payload []byte, objectSpecIndex map[string]sourceBackedObjectSpecTarget, packObjectSpecs []ObjectSpec) (*sourceBackedCustomFieldComponent, error) {
	var ref sourceBackedCustomFieldPayload
	if err := json.Unmarshal(payload, &ref); err != nil {
		return nil, fmt.Errorf("parse custom_field payload: %w", err)
	}
	ref.ObjectAPIName = strings.TrimSpace(ref.ObjectAPIName)
	ref.FieldAPIName = strings.TrimSpace(ref.FieldAPIName)
	if ref.ObjectAPIName == "" || ref.FieldAPIName == "" {
		return nil, fmt.Errorf("custom_field payload requires objectApiName and fieldApiName")
	}

	expectedBusinessKey := JoinComponentBusinessKey(ref.ObjectAPIName, ref.FieldAPIName)
	if got := strings.TrimSpace(manifestComponent.BusinessKey); got != expectedBusinessKey {
		return nil, fmt.Errorf("custom_field business key = %q, want %q", got, expectedBusinessKey)
	}

	declared, err := resolveDeclaredCustomFieldExtension(packObjectSpecs, ref.ObjectAPIName, ref.FieldAPIName)
	if err != nil {
		return nil, err
	}
	target, ok := objectSpecIndex[strings.ToLower(ref.ObjectAPIName)]
	if !ok {
		return nil, fmt.Errorf("custom_field %s/%s requires declaring object_spec in the same pack", ref.ObjectAPIName, ref.FieldAPIName)
	}
	if target.manageability != ManageabilityLocked {
		return nil, fmt.Errorf("custom_field %s/%s requires locked object_spec backing, got %q", ref.ObjectAPIName, ref.FieldAPIName, target.manageability)
	}
	if !hasComponentDependency(manifestComponent.DependsOn, ComponentKindObjectSpec, target.businessKey) {
		return nil, fmt.Errorf("custom_field %s/%s requires dependsOn on object_spec %q", ref.ObjectAPIName, ref.FieldAPIName, target.businessKey)
	}

	return &sourceBackedCustomFieldComponent{
		packName:          packName,
		manifestComponent: manifestComponent,
		payload:           append([]byte(nil), payload...),
		declared:          declared,
	}, nil
}

func (c *sourceBackedCustomFieldComponent) Kind() ComponentKind {
	return ComponentKindCustomField
}

func (c *sourceBackedCustomFieldComponent) PackName() string {
	return c.packName
}

func (c *sourceBackedCustomFieldComponent) BusinessKey() string {
	return strings.TrimSpace(c.manifestComponent.BusinessKey)
}

func (c *sourceBackedCustomFieldComponent) Manageability() Manageability {
	return c.manifestComponent.Manageability
}

func (c *sourceBackedCustomFieldComponent) ExpectedPayload() ([]byte, error) {
	return append([]byte(nil), c.payload...), nil
}

func (c *sourceBackedCustomFieldComponent) ExpectedProjection() (any, error) {
	return c.expectedProjection(), nil
}

func (c *sourceBackedCustomFieldComponent) LiveProjection(ctx context.Context, deps VerifyDeps) (any, error) {
	declared, err := c.loadLiveDeclaredField(ctx, deps.DB)
	if err != nil {
		return nil, err
	}
	return clonePackComponentProjection(declared)
}

func (c *sourceBackedCustomFieldComponent) VerifyComponent(ctx context.Context, deps VerifyDeps) error {
	_, err := c.loadLiveDeclaredField(ctx, deps.DB)
	return err
}

func (c *sourceBackedCustomFieldComponent) Apply(ctx context.Context, deps ApplyDeps) error {
	_, err := c.loadLiveDeclaredField(ctx, deps.DB)
	return err
}

func (c *sourceBackedCustomFieldComponent) DependsOn() []ComponentReference {
	return append([]ComponentReference(nil), c.manifestComponent.DependsOn...)
}

func hasComponentDependency(deps []ComponentReference, kind ComponentKind, businessKey string) bool {
	for _, dep := range deps {
		if dep.Kind == kind && strings.TrimSpace(dep.BusinessKey) == strings.TrimSpace(businessKey) {
			return true
		}
	}
	return false
}

func (c *sourceBackedCustomFieldComponent) loadLiveDeclaredField(ctx context.Context, db *sql.DB) (declaredCustomFieldExtension, error) {
	projection, err := c.loadLiveProjection(ctx, db)
	if err != nil {
		return declaredCustomFieldExtension{}, err
	}
	if projection != c.expectedProjection() {
		return declaredCustomFieldExtension{}, fmt.Errorf(
			"custom field %s.%s projection = %#v, want %#v",
			c.declared.ObjectAPIName,
			c.declared.Field.APIName,
			projection,
			c.expectedProjection(),
		)
	}
	return c.declared, nil
}

type customFieldProjection struct {
	ObjectAPIName string
	FieldAPIName  string
	OwnerKind     string
	OwnerPack     string
	IsCustomized  bool
}

func (c *sourceBackedCustomFieldComponent) expectedProjection() customFieldProjection {
	return customFieldProjection{
		ObjectAPIName: c.declared.ObjectAPIName,
		FieldAPIName:  c.declared.Field.APIName,
		OwnerKind:     constants.FieldOwnerKindPack,
		OwnerPack:     c.packName,
		IsCustomized:  false,
	}
}

func (c *sourceBackedCustomFieldComponent) loadLiveProjection(ctx context.Context, db *sql.DB) (customFieldProjection, error) {
	if db == nil {
		return customFieldProjection{}, fmt.Errorf("db is unavailable for custom_field verification")
	}

	var projection customFieldProjection
	err := db.QueryRowContext(ctx, `
SELECT o.ApiName, f.ApiName, COALESCE(f.OwnerKind, ''), COALESCE(f.OwnerPack, ''), COALESCE(f.IsCustomized, 0)
FROM _System_Field f
JOIN _System_Object o ON o.Id = f.ObjectId
WHERE o.ApiName = ? AND f.ApiName = ?
LIMIT 1`, c.declared.ObjectAPIName, c.declared.Field.APIName).Scan(
		&projection.ObjectAPIName,
		&projection.FieldAPIName,
		&projection.OwnerKind,
		&projection.OwnerPack,
		&projection.IsCustomized,
	)
	if err == sql.ErrNoRows {
		return customFieldProjection{}, fmt.Errorf("custom field %s.%s is missing", c.declared.ObjectAPIName, c.declared.Field.APIName)
	}
	if err != nil {
		return customFieldProjection{}, fmt.Errorf("query custom field %s.%s: %w", c.declared.ObjectAPIName, c.declared.Field.APIName, err)
	}
	return projection, nil
}
