package main

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"strings"
	"time"

	"git.dev.alexdunmow.com/block/pluginsdk/crypto"
	"git.dev.alexdunmow.com/block/pluginsdk/settings"

	"git.dev.alexdunmow.com/block/calcomblock/internal/db"
	"git.dev.alexdunmow.com/block/calcomblock/internal/helpers"
)

// SettingsManager persists Cal.com plugin settings through the SDK settings
// capabilities (a capabilityQuerier satisfying helpers.PluginSettingsQuerier in
// production; an in-memory fake in tests). Secrets are encrypted using the
// SDK-provided crypto.Crypto interface.
type SettingsManager struct {
	crypto  crypto.Crypto
	queries helpers.PluginSettingsQuerier
}

// NewSettingsManager constructs a SettingsManager backed by the given crypto
// and querier. The querier must satisfy helpers.PluginSettingsQuerier.
func NewSettingsManager(c crypto.Crypto, q helpers.PluginSettingsQuerier) *SettingsManager {
	return &SettingsManager{crypto: c, queries: q}
}

// GetAPIKey returns the decrypted Cal.com API key, or empty string if not configured.
func (m *SettingsManager) GetAPIKey(ctx context.Context) (string, error) {
	s, err := helpers.GetPluginSettings(ctx, m.queries, "calcomblock")
	if err != nil {
		return "", err
	}
	enc := helpers.GetStringOr(s, "api_key_encrypted", "")
	if enc == "" {
		return "", nil
	}
	return m.crypto.DecryptSecret(enc)
}

// SetAPIKey encrypts and stores the Cal.com API key. Pass an empty string to clear.
func (m *SettingsManager) SetAPIKey(ctx context.Context, key string) error {
	s, err := helpers.GetPluginSettings(ctx, m.queries, "calcomblock")
	if err != nil {
		return err
	}
	if key == "" {
		delete(s, "api_key_encrypted")
	} else {
		enc, err := m.crypto.EncryptSecret(key)
		if err != nil {
			return err
		}
		s["api_key_encrypted"] = enc
	}
	return helpers.SetPluginSettings(ctx, m.queries, "calcomblock", s)
}

// GetSettings returns the full settings blob. Secrets are NOT decrypted here —
// the caller is responsible for not exposing `api_key_encrypted` directly.
func (m *SettingsManager) GetSettings(ctx context.Context) (map[string]any, error) {
	return helpers.GetPluginSettings(ctx, m.queries, "calcomblock")
}

// GetUsername returns the Cal.com account username associated with the
// configured API key. Stored as plain text (not a secret); populated by
// HandleSaveSettings via CalcomClient.GetMe. Empty string when the API key
// has never been saved or the /me lookup has not yet succeeded.
func (m *SettingsManager) GetUsername(ctx context.Context) (string, error) {
	s, err := helpers.GetPluginSettings(ctx, m.queries, "calcomblock")
	if err != nil {
		return "", err
	}
	return helpers.GetStringOr(s, "username", ""), nil
}

// SetUsername persists the Cal.com account username. Pass an empty string to
// clear (e.g. when the API key is removed).
func (m *SettingsManager) SetUsername(ctx context.Context, username string) error {
	s, err := helpers.GetPluginSettings(ctx, m.queries, "calcomblock")
	if err != nil {
		return err
	}
	if username == "" {
		delete(s, "username")
	} else {
		s["username"] = username
	}
	return helpers.SetPluginSettings(ctx, m.queries, "calcomblock", s)
}

// GetSiteTimezone returns the site-wide IANA timezone configured under Site
// Settings → Localization, or "" when unset. Used as the fallback display
// timezone for booking times when the visitor's request carries none.
//
// Reads the "site" settings key through the querier, which resolves it via the
// SDK settings capability (settings.Settings.GetSiteSettings) host-side.
func (m *SettingsManager) GetSiteTimezone(ctx context.Context) string {
	setting, err := m.queries.GetSetting(ctx, "site")
	if err != nil {
		return ""
	}
	var site struct {
		Localization struct {
			Timezone string `json:"timezone"`
		} `json:"localization"`
	}
	if err := json.Unmarshal(setting.Value, &site); err != nil {
		return ""
	}
	return strings.TrimSpace(site.Localization.Timezone)
}

// resolveLocation resolves the IANA timezone used to window and display
// booking times. Resolution order: the visitor-supplied name (browser-detected,
// user-controlled input) when it parses, else the site-wide Localization
// timezone from site settings, else UTC. A literal "UTC" request is treated as
// unset: it is the widget's never-updated sentinel default (pages cached from
// before timezone detection, no-JS visitors, bots), so the site timezone is a
// strictly better guess for a human visitor.
//
// Returns the location plus its canonical name for forwarding to Cal.com.
// Lives on SettingsManager (not the handler) so the initial block render —
// which has no *http.Request — can resolve via the same ladder.
func (m *SettingsManager) resolveLocation(ctx context.Context, requested string) (*time.Location, string) {
	for _, name := range []string{strings.TrimSpace(requested), m.GetSiteTimezone(ctx)} {
		if name == "" || strings.EqualFold(name, "UTC") {
			continue
		}
		if loc, err := time.LoadLocation(name); err == nil {
			return loc, name
		}
	}
	return time.UTC, "UTC"
}

// GetWebhookSecret returns the decrypted webhook HMAC secret, or empty if none
// has been provisioned yet.
func (m *SettingsManager) GetWebhookSecret(ctx context.Context) (string, error) {
	s, err := helpers.GetPluginSettings(ctx, m.queries, "calcomblock")
	if err != nil {
		return "", err
	}
	enc := helpers.GetStringOr(s, "webhook_secret_encrypted", "")
	if enc == "" {
		return "", nil
	}
	return m.crypto.DecryptSecret(enc)
}

// SetWebhookSecret encrypts and persists the webhook HMAC secret. Pass an
// empty string to clear.
func (m *SettingsManager) SetWebhookSecret(ctx context.Context, secret string) error {
	s, err := helpers.GetPluginSettings(ctx, m.queries, "calcomblock")
	if err != nil {
		return err
	}
	if secret == "" {
		delete(s, "webhook_secret_encrypted")
	} else {
		enc, err := m.crypto.EncryptSecret(secret)
		if err != nil {
			return err
		}
		s["webhook_secret_encrypted"] = enc
	}
	return helpers.SetPluginSettings(ctx, m.queries, "calcomblock", s)
}

// UpdateLastEvent records that the webhook handler received a Cal.com event
// at the given time. Surfaced to the admin settings panel via GetSettings.
func (m *SettingsManager) UpdateLastEvent(ctx context.Context, triggerEvent, createdAt string) error {
	s, err := helpers.GetPluginSettings(ctx, m.queries, "calcomblock")
	if err != nil {
		return err
	}
	s["last_event_type"] = triggerEvent
	s["last_event_at"] = createdAt
	return helpers.SetPluginSettings(ctx, m.queries, "calcomblock", s)
}

// generateWebhookSecret returns a fresh 32-byte secret encoded as a 64-char
// hex string. Cryptographically random via crypto/rand.
func generateWebhookSecret() (string, error) {
	buf := make([]byte, 32)
	if _, err := rand.Read(buf); err != nil {
		return "", err
	}
	return hex.EncodeToString(buf), nil
}

// capabilityQuerier adapts the SDK settings capabilities to
// helpers.PluginSettingsQuerier. Under the wasm ABI a plugin's Postgres role is
// sandboxed to its OWN schema, so the CMS `settings` table (public schema) is
// unreachable by direct SQL — settings persistence MUST go through the
// host-side capability interfaces (settings.Settings / settings.Updater), which
// perform the DB work on the host's behalf.
//
// It recognises the two keys the settings helpers request: "plugin:<name>"
// (round-tripped through Get/UpdatePluginSettings) and "site" (read-only via
// GetSiteSettings). Values are JSON, matching the shape the helpers expect from
// the old `settings.value` bytea column.
type capabilityQuerier struct {
	settings settings.Settings
	updater  settings.Updater
}

func (q *capabilityQuerier) GetSetting(ctx context.Context, key string) (db.Setting, error) {
	var (
		m   map[string]any
		err error
	)
	if key == "site" {
		m, err = q.settings.GetSiteSettings(ctx)
	} else {
		m, err = q.settings.GetPluginSettings(ctx, strings.TrimPrefix(key, "plugin:"))
	}
	if err != nil {
		return db.Setting{}, err
	}
	value, err := json.Marshal(m)
	if err != nil {
		return db.Setting{}, err
	}
	return db.Setting{Key: key, Value: value}, nil
}

func (q *capabilityQuerier) UpsertSetting(ctx context.Context, arg db.UpsertSettingParams) (db.Setting, error) {
	var m map[string]any
	if len(arg.Value) > 0 {
		if err := json.Unmarshal(arg.Value, &m); err != nil {
			return db.Setting{}, err
		}
	}
	if m == nil {
		m = map[string]any{}
	}
	if err := q.updater.UpdatePluginSettings(ctx, strings.TrimPrefix(arg.Key, "plugin:"), m); err != nil {
		return db.Setting{}, err
	}
	return db.Setting(arg), nil
}
