package packs

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

	"github.com/nexuscrm/kernel/pkg/utils"
)

// webhookSecretDefinition is the payload shape for a single webhook secret
// declared by a connector pack. The definition is pack-owned (L1); the
// corresponding runtime secret bytes live in _System_WebhookSecretValue (L2).
type webhookSecretDefinition struct {
	// Name is the webhook path / usage identifier, unique within the owning pack.
	Name string `json:"name"`
	// Algorithm is the HMAC algorithm, e.g. "HMAC-SHA256".
	Algorithm string `json:"algorithm"`
	// Description is an optional admin-readable note.
	Description *string `json:"description,omitempty"`
}

// webhookSecretProjectionRow is the expected/live projection shape used by
// verifyPackComponentDefault to detect drift.
type webhookSecretProjectionRow struct {
	OwnerPack   string  `json:"ownerPack"`
	Name        string  `json:"name"`
	Algorithm   string  `json:"algorithm"`
	Description *string `json:"description,omitempty"`
}

// webhookSecretComponent implements PackComponent for ComponentKindWebhookSecret.
// It owns exactly one row in _System_WebhookSecret. The component is locked
// (admins cannot override algorithm or name — they are L1 product facts).
type webhookSecretComponent struct {
	packName   string
	definition webhookSecretDefinition
	payload    []byte
}

// NewWebhookSecretComponent constructs a webhookSecretComponent from a parsed
// definition. Use this from pack-source parsing or test fixtures.
func NewWebhookSecretComponent(packName string, def webhookSecretDefinition) (PackComponent, error) {
	packName = strings.ToLower(strings.TrimSpace(packName))
	if packName == "" {
		return nil, fmt.Errorf("webhook secret component: packName is required")
	}
	def.Name = strings.ToLower(strings.TrimSpace(def.Name))
	if def.Name == "" {
		return nil, fmt.Errorf("webhook secret component: name is required")
	}
	def.Algorithm = strings.TrimSpace(def.Algorithm)
	if def.Algorithm == "" {
		return nil, fmt.Errorf("webhook secret component: algorithm is required")
	}
	payload, err := json.Marshal(def)
	if err != nil {
		return nil, fmt.Errorf("webhook secret component: marshal payload: %w", err)
	}
	return &webhookSecretComponent{
		packName:   packName,
		definition: def,
		payload:    payload,
	}, nil
}

func (c *webhookSecretComponent) Kind() ComponentKind {
	return ComponentKindWebhookSecret
}

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

func (c *webhookSecretComponent) BusinessKey() string {
	return JoinComponentBusinessKey(c.packName, c.definition.Name)
}

func (c *webhookSecretComponent) Manageability() Manageability {
	return ManageabilityLocked
}

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

func (c *webhookSecretComponent) ExpectedProjection() (any, error) {
	return clonePackComponentProjection(webhookSecretProjectionRow{
		OwnerPack:   c.packName,
		Name:        c.definition.Name,
		Algorithm:   c.definition.Algorithm,
		Description: c.definition.Description,
	})
}

func (c *webhookSecretComponent) LiveProjection(ctx context.Context, deps VerifyDeps) (any, error) {
	return liveWebhookSecretProjection(ctx, deps.DB, c.packName, c.definition.Name)
}

func (c *webhookSecretComponent) Apply(ctx context.Context, deps ApplyDeps) error {
	return upsertWebhookSecretDefinition(ctx, deps.DB, c.packName, c.definition)
}

// upsertWebhookSecretDefinition inserts or updates a row in _System_WebhookSecret.
// The UNIQUE (OwnerPack, Name) constraint ensures idempotent upserts.
// Both ownerPack and Name are normalised to lowercase at intake so that
// ExpectedProjection / LiveProjection comparisons are case-stable and a
// case-only manifest rename converges the stored row rather than diverging.
func upsertWebhookSecretDefinition(ctx context.Context, db *sql.DB, ownerPack string, def webhookSecretDefinition) error {
	normalizedOwnerPack := strings.ToLower(strings.TrimSpace(ownerPack))
	normalizedName := strings.ToLower(strings.TrimSpace(def.Name))
	var descParam interface{}
	if def.Description != nil {
		descParam = *def.Description
	}
	_, err := db.ExecContext(ctx, `
INSERT INTO _System_WebhookSecret (Id, OwnerPack, Name, Algorithm, Description, IsDeleted, CreatedDate, LastModifiedDate)
VALUES (?, ?, ?, ?, ?, 0, NOW(), NOW())
ON DUPLICATE KEY UPDATE
  Name = VALUES(Name),
  Algorithm = VALUES(Algorithm),
  Description = VALUES(Description),
  IsDeleted = 0,
  LastModifiedDate = NOW()`,
		utils.GenerateID(), normalizedOwnerPack, normalizedName, def.Algorithm, descParam,
	)
	if err != nil {
		return fmt.Errorf("upsert _System_WebhookSecret %s/%s: %w", normalizedOwnerPack, normalizedName, err)
	}
	return nil
}

// liveWebhookSecretProjection reads the live row from _System_WebhookSecret and
// returns the same projection shape as ExpectedProjection.
func liveWebhookSecretProjection(ctx context.Context, db *sql.DB, ownerPack, name string) (any, error) {
	var row webhookSecretProjectionRow
	var description sql.NullString
	err := db.QueryRowContext(ctx, `
SELECT OwnerPack, Name, Algorithm, Description
FROM _System_WebhookSecret
WHERE OwnerPack = ? AND Name = ? AND IsDeleted = 0
LIMIT 1`, ownerPack, name).Scan(&row.OwnerPack, &row.Name, &row.Algorithm, &description)
	if err == sql.ErrNoRows {
		return nil, fmt.Errorf("webhook secret %s/%s: row missing or soft-deleted", ownerPack, name)
	}
	if err != nil {
		return nil, fmt.Errorf("query webhook secret %s/%s: %w", ownerPack, name, err)
	}
	if description.Valid {
		s := description.String
		row.Description = &s
	}
	return row, nil
}
