<?php

namespace GriffinForms\Includes\Integrations;

use GriffinForms\Settings;
use GriffinForms\Log;
use GriffinForms\Config;
use GriffinForms\Includes\Integrations\Providers;
use GriffinForms\Includes\Addons\Registry as AddonsRegistry;
use WP_Error;

/**
 * Base Format class for all integrations
 *
 * All integrations extend this class to provide a standardized interface.
 * Implements the pattern for integration metadata, configuration, and status checking.
 *
 * ARCHITECTURE & DESIGN PATTERNS:
 * ==============================
 *
 * Template Method Pattern:
 * - getInfo() - Caches result from buildInfo() on first call
 * - testConnection() - Validates configuration then delegates to validateConnection()
 * - saveConfig() - Standardizes response format from saveConfiguration()
 *
 * Concrete Methods with Abstract Hooks:
 * - getConfigurationFormTemplate() renders standard form HTML
 * - Subclasses only need to build field HTML, not wrapper HTML
 *
 * PERFORMANCE OPTIMIZATIONS:
 * ==========================
 *
 * 1. Option Caching (per-request):
 *    - getOption() caches values to eliminate N+1 queries
 *    - saveOption()/deleteOption() invalidate cache on modification
 *    - getAllOptions() uses pre-fetched values (eliminates 1+N lookups)
 *    - Significant savings on pages that call isConfigured() or getStatus()
 *
 * 2. Metadata Caching:
 *    - getInfo() caches result to avoid array rebuilding on multiple calls
 *    - Called by isGlobal() and allowsOverride(), so caching is important
 *
 * 3. Code Deduplication (DRY):
 *    - ~135 lines of duplicate form HTML template eliminated
 *    - ~20 lines of duplicate testConnection pattern eliminated
 *    - ~18 lines of duplicate saveConfig wrapper eliminated
 *    - All three integrations now share base implementations
 *
 * Usage:
 * - Create a new integration by extending this class
 * - Implement abstract methods: setMeta(), buildInfo(), getStatus(), getConfigFields(),
 *   saveConfiguration(), validateConnection(), isLibraryInstalled(), getLibraryInstructions()
 * - Register in Providers.php
 * - Optionally override: onDisable() for custom cleanup logic
 *
 * @since 1.0.0
 */
abstract class Format
{
    protected $settings;
    protected $log;
    protected $config;
    protected $addons;
    protected $id;           // Integration ID (stripe, sendgrid, etc.)
    protected $meta;         // Settings key prefix (e.g., 'stripe_api_key')
    protected $option_group = 'integrations'; // Settings group used when inserting new options via saveOption()
    private $option_cache = [];  // Cache for option values to avoid redundant lookups
    private $info_cache = null;  // Cache for getInfo() result
    protected $form_context_id = 0; // Active form context for logging/per-form behavior
    protected $form_field_option_cache = null; // Cached [field_id => label] map for the current form context
    protected $form_field_meta_cache   = null; // Cached [field_id => ['type'=>..,'label'=>..]] map

    public function __construct()
    {
        $this->settings = Settings::getInstance();
        $this->log = Log::getInstance();
        $this->config = Config::getInstance();
        $this->addons = AddonsRegistry::getInstance();
        $this->setMeta();
    }

    /**
     * Set integration metadata key prefix
     * Called during initialization
     *
     * Example: protected function setMeta() { $this->meta = 'stripe'; }
     */
    abstract protected function setMeta();

    /**
     * Get integration metadata/info (with caching)
     *
     * Returns complete metadata for the integration including:
     * - Basic info: id, name, category, description
     * - Classification: enabled, is_paid, is_beta, priority, tags
     * - Status: configured, active, requires_lib
     * - References: class, icon, documentation URLs
     * - Requirements: php version, required modules
     *
     * Caches result to avoid rebuilding the array on multiple calls.
     *
     * @return array Integration metadata
     */
    final public function getInfo(): array
    {
        // Return cached info if available
        if ($this->info_cache !== null) {
            return $this->info_cache;
        }

        // Build and cache the info
        $this->info_cache = $this->buildInfo();
        return $this->info_cache;
    }

    /**
     * Schema for per-form settings (optional, override in child).
     *
     * @return array
     */
    public function getFormSettingsSchema(): array
    {
        return [];
    }

    /**
     * Default per-form settings (optional, override in child).
     *
     * @return array
     */
    public function getDefaultFormSettings(): array
    {
        return [];
    }

    /**
     * Resolve an AI provider API key from WP7 Connectors, falling back to empty.
     *
     * Checks in the same order WP Core does: environment variable → PHP constant → wp_options.
     * Returns empty string on WP6 (function_exists guard) or when no key is found.
     * Never called when the GriffinForms own option already has a value — that takes precedence.
     *
     * @param string $connector_id  WP Connector ID (e.g. 'openai', 'anthropic', 'google').
     * @return string Raw API key or empty string.
     */
    protected function resolveConnectorApiKey( string $connector_id ): string {
        if ( ! function_exists( 'wp_get_connector' ) ) {
            return '';
        }

        $connector = wp_get_connector( $connector_id );
        if ( ! $connector || empty( $connector['authentication']['setting_name'] ) ) {
            return '';
        }

        $auth = $connector['authentication'];

        // Environment variable takes highest priority.
        if ( ! empty( $auth['env_var_name'] ) ) {
            $env_value = getenv( $auth['env_var_name'] );
            if ( false !== $env_value && '' !== $env_value ) {
                return trim( $env_value );
            }
        }

        // PHP constant next.
        if ( ! empty( $auth['constant_name'] ) && defined( $auth['constant_name'] ) ) {
            $const_value = constant( $auth['constant_name'] );
            if ( is_string( $const_value ) && '' !== $const_value ) {
                return trim( $const_value );
            }
        }

        // Database last.
        return trim( (string) get_option( $auth['setting_name'], '' ) );
    }

    /**
     * Set current form context for the integration
     */
    public function setFormContextId(int $form_id): void
    {
        $this->form_context_id         = max(0, $form_id);
        $this->form_field_option_cache = null;
        $this->form_field_meta_cache   = null;
    }

    /**
     * Validate per-form settings (optional, override in child).
     *
     * @param array $settings
     * @return array|WP_Error Sanitized settings or error
     */
    public function validateFormSettings(array $settings)
    {
        return $settings;
    }

    /**
     * Build integration metadata/info
     *
     * Subclasses must implement this method to return integration metadata.
     * This method is called only once per request and its result is cached.
     *
     * @return array Integration metadata
     */
    abstract protected function buildInfo(): array;

    /**
     * Get current integration status
     *
     * Returns status information including:
     * - configured: Has user provided necessary credentials?
     * - active: Is the integration currently enabled?
     * - message: Human-readable status message
     * - last_test: When was connection last tested?
     * - test_passed: Did the last test succeed?
     *
     * @return array Status array with keys: configured, active, message, etc.
     */
    abstract public function getStatus(): array;

    /**
     * Get configuration form fields
     *
     * Returns array of form field definitions for configuration form.
     * Each field defines: name, label, type, required, help, encrypted, etc.
     *
     * Example:
     * ```php
     * return [
     *     ['name' => 'api_key', 'label' => 'API Key', 'type' => 'password', 'required' => true],
     * ];
     * ```
     *
     * @return array Form field definitions
     */
    abstract public function getConfigFields(): array;

    /**
     * Save integration configuration
     *
     * Provides a standard wrapper that calls saveConfiguration() and returns
     * a consistent response format across all integrations.
     *
     * @param array $config Configuration data from user form
     * @return array ['success' => bool, 'message' => string]
     */
    final public function saveConfig(array $config): array
    {
        $success = $this->saveConfiguration($config);
        return [
            'success' => $success,
            'message' => $success
                ? __('Configuration saved successfully', 'griffinforms-form-builder')
                : __('Failed to save configuration', 'griffinforms-form-builder'),
        ];
    }

    /**
     * Save integration configuration (implementation)
     *
     * Subclasses must implement this to validate and save user-provided configuration.
     * Should include input validation, sanitization, and encryption for sensitive data.
     *
     * @param array $config Configuration data from user form
     * @return bool True if saved successfully, false otherwise
     */
    abstract protected function saveConfiguration(array $config): bool;

    /**
     * Test integration connection
     *
     * Verifies that the integration is properly configured and can connect.
     * Implements base validation check (configuration) and delegates to subclass
     * for actual API validation.
     *
     * @return array ['success' => bool, 'message' => string, 'error' => string|null]
     */
    final public function testConnection(): array
    {
        if (!$this->isConfigured()) {
            return [
                'success' => false,
                /* translators: %s: integration name */
                'message' => sprintf(__('%s is not configured', 'griffinforms-form-builder'), $this->getInfo()['name'] ?? 'Integration'),
            ];
        }

        // Delegate actual validation to subclass
        return $this->validateConnection();
    }

    /**
     * Validate connection with third-party service
     *
     * Subclasses must implement this to perform actual validation
     * (e.g., API call to verify credentials).
     *
     * Called by testConnection() after verifying local configuration.
     *
     * @return array ['success' => bool, 'message' => string, 'error' => string|null]
     */
    abstract protected function validateConnection(): array;

    /**
     * Check if external library is installed
     *
     * Some integrations require external libraries (e.g., Stripe PHP SDK).
     * This method checks if the required library is available.
     *
     * @return bool True if library is installed and available
     */
    abstract public function isLibraryInstalled(): bool;

    /**
     * Get library installation instructions
     *
     * Returns HTML instructions for installing the required external library.
     * Typically includes composer command or download link.
     *
     * @return string Installation guide HTML
     */
    abstract public function getLibraryInstructions(): string;

    // ========== Helper Methods (available to all integrations) ==========

    /**
     * Get a saved integration option
     *
     * Caches option values to avoid redundant database lookups within a single request.
     * Useful when multiple methods need the same option values.
     *
     * @param string $key Option key (without prefix)
     * @param mixed  $default Default value if not found
     * @return mixed Option value or default
     */
    protected function getOption(string $key, $default = null)
    {
        $full_key = $this->meta . '_' . $key;

        // Return cached value if available
        if (isset($this->option_cache[$full_key])) {
            return $this->option_cache[$full_key];
        }

        // Fetch from settings and cache the result
        $value = $this->settings->getOption($full_key, $default);
        $this->option_cache[$full_key] = $value;

        return $value;
    }

    /**
     * Save an integration option
     *
     * Invalidates cache entry for this key to ensure fresh value on next read.
     *
     * @param string $key Option key (without prefix)
     * @param mixed  $value Value to save
     * @return bool True on success
     */
    protected function saveOption(string $key, $value): bool
    {
        $full_key = $this->meta . '_' . $key;
        $result = $this->settings->updateOption($full_key, $value, $this->option_group);

        // Invalidate cache for this key after save
        if ($result) {
            unset($this->option_cache[$full_key]);
        }

        return $result;
    }

    /**
     * Delete an integration option
     *
     * Invalidates cache entry for this key after deletion.
     *
     * @param string $key Option key (without prefix)
     * @return bool True on success
     */
    protected function deleteOption(string $key): bool
    {
        $full_key = $this->meta . '_' . $key;
        $result = $this->settings->deleteOption($full_key);

        // Invalidate cache for this key after delete
        if ($result) {
            unset($this->option_cache[$full_key]);
        }

        return $result;
    }

    /**
     * Get all options for this integration
     *
     * Optimized to avoid N+1 queries: uses values directly from initial getAllOptions()
     * call instead of making additional getOption() calls for each key.
     *
     * @return array All integration options with keys (unprefixed)
     */
    protected function getAllOptions(): array
    {
        $all_options = $this->settings->getAllOptions();
        $prefix = $this->meta . '_';
        $prefix_length = strlen($prefix);
        $integration_options = [];

        foreach ($all_options as $key => $value) {
            if (strpos($key, $prefix) === 0) {
                $unprefixed_key = substr($key, $prefix_length);
                // Use value directly from getAllOptions() instead of calling getOption() again
                // This avoids N+1 database queries
                $integration_options[$unprefixed_key] = $value;
            }
        }

        return $integration_options;
    }

    /**
     * Clear all options for this integration
     *
     * Useful for uninstalling or resetting the integration.
     * Called when disabling an integration to remove all stored data.
     *
     * @return bool True on success
     */
    public function clearAllOptions(): bool
    {
        $all_options = $this->getAllOptions();
        foreach (array_keys($all_options) as $key) {
            $this->deleteOption($key);
        }

        // Clear entire option cache after bulk delete
        $this->clearOptionCache();

        // Call onDisable hook for custom cleanup logic
        $this->onDisable();

        return true;
    }

    /**
     * Clear the option cache
     *
     * Call this after bulk operations that modify multiple options,
     * or before operations that need fresh data from the database.
     *
     * @return void
     */
    protected function clearOptionCache(): void
    {
        $this->option_cache = [];
    }

    /**
     * Hook called when integration is disabled
     *
     * Override this method in subclasses to perform custom cleanup logic
     * when the integration is disabled. Called after clearAllOptions().
     *
     * @return void
     */
    protected function onDisable()
    {
        // Default: no additional cleanup needed
    }

    /**
     * Get integration ID
     *
     * @return string Integration ID
     */
    public function getId(): string
    {
        return $this->id;
    }

    /**
     * Get integration meta (settings prefix)
     *
     * @return string Settings key prefix
     */
    public function getMeta(): string
    {
        return $this->meta;
    }

    /**
     * Get integration icon URL from provider metadata
     *
     * Retrieves the icon URL from the integration's provider definition
     * instead of hardcoding the path. Ensures consistency across all views.
     *
     * @return string Icon URL from provider metadata, or empty string if not found
     */
    public function getIconUrl(): string
    {
        try {
            // Get provider metadata which contains the icon URL
            $providers = Providers::getInstance();
            $provider = $providers->get($this->id, false);

            if ($provider && isset($provider['icon'])) {
                return $provider['icon'];
            }
        } catch (\Exception $e) {
            // Silently fail and return empty string
        }

        return '';
    }

    // ========== Scope & Availability Helper Methods ==========

    /**
     * Check if integration works globally (plugin-wide)
     *
     * Returns true if this integration is configured at plugin level,
     * false if it's configured per-form.
     *
     * @return bool True if global, false if per-form
     */
    public function isGlobal(): bool
    {
        $info = $this->getInfo();
        return $info['scope']['is_global'] ?? false;
    }

    /**
     * Check if global integration can be overridden per-form
     *
     * Only meaningful if isGlobal() returns true.
     * Returns true if users can override global settings on a per-form basis.
     *
     * @return bool True if overridable, false if locked to global
     */
    public function allowsOverride(): bool
    {
        $info = $this->getInfo();
        return (($info['scope']['is_global'] ?? false) && ($info['scope']['allow_override'] ?? false));
    }

    // ========== Configuration Form Template (Reusable) ==========

    /**
     * Render configuration form with standard template
     *
     * Provides a reusable HTML template for integration configuration forms.
     * Eliminates duplication across all integrations.
     *
     * @param string $form_id HTML ID for the form (e.g., 'gf-stripe-config-form')
     * @param string $integration_id Integration ID (e.g., 'stripe')
     * @param string $form_fields_html HTML content for form fields (rendered from factory)
     * @return string Complete form HTML
     */
    protected function renderConfigurationFormTemplate(
        string $form_id,
        string $integration_id,
        string $form_fields_html,
        array $options = []
    ): string {
        $info = $this->getInfo();
        $name = $info['name'] ?? '';
        $category = $info['category'] ?? '';
        $icon_path = $this->getIconUrl();

        ob_start();
        ?>
        <div class="gf-integration-config-wrapper d-flex" style="gap: 2rem;">
            <!-- Left Sidebar with Integration Logo & Title -->
            <div class="gf-integration-config-sidebar" style="flex: 0 0 160px; padding-top: 0.5rem;">
                <div style="text-align: center;">
                    <img src="<?php echo esc_url($icon_path); ?>"
                         alt="<?php echo esc_attr($name); ?>"
                         style="width: 60px; height: 60px; margin-bottom: 1rem; display: block; margin-left: auto; margin-right: auto;">
                    <h3 style="margin: 0 0 0.5rem 0; font-size: 15px; font-weight: 600; color: #1f2937;">
                        <?php echo esc_html($name); ?>
                    </h3>
                    <p style="margin: 0; font-size: 11px; color: #999; text-transform: uppercase; letter-spacing: 0.5px;">
                        <?php echo esc_html($category); ?>
                    </p>
                </div>
            </div>

            <!-- Right Content with Form -->
            <div class="gf-integration-config-content" style="flex: 1; min-width: 0;">
                <form id="<?php echo esc_attr($form_id); ?>" class="gf-integration-config-form">
                    <!-- Form fields rendered by subclass -->
                    <?php echo $form_fields_html; ?>

                    <!-- Form actions -->
                    <div class="gf-form-actions" style="display: flex; gap: 0.75rem; margin-top: 2rem; flex-wrap: wrap; align-items: center;">
                        <button type="submit" class="gf-btn gf-btn-primary">
                            <?php esc_html_e('Save Configuration', 'griffinforms-form-builder'); ?>
                        </button>
                        <button type="button" class="gf-btn gf-btn-secondary gf-integration-btn-cancel">
                            <?php esc_html_e('Cancel', 'griffinforms-form-builder'); ?>
                        </button>
                        <?php if ( ! empty( $options['show_remove'] ) ) : ?>
                        <button type="button" class="gf-btn gf-integration-btn-remove" style="margin-left: auto; color: #b91c1c;">
                            <?php esc_html_e('Remove Key', 'griffinforms-form-builder'); ?>
                        </button>
                        <?php endif; ?>
                    </div>
                </form>
            </div>
        </div>
        <?php
        return ob_get_clean();
    }

    /**
     * Return a NotificationChannelInterface if this integration can act as a notification channel,
     * or null if it has no notification capability. Override in integrations that support sending.
     */
    public function getNotificationChannel(): ?\GriffinForms\Includes\Notifications\NotificationChannelInterface
    {
        return null;
    }

    /**
     * Resolve current form ID from request context
     */
    protected function resolveFormContextId(): int
    {
        if ($this->form_context_id > 0) {
            return $this->form_context_id;
        }

        if (!empty($_GET['form_id'])) {
            return absint(wp_unslash($_GET['form_id']));
        }

        if (!empty($_POST['form_id'])) {
            return absint(wp_unslash($_POST['form_id']));
        }

        return 0;
    }

    // ─── Type coercion helpers ────────────────────────────────────────────────

    protected function toBoolean($value): bool
    {
        if (is_bool($value)) {
            return $value;
        }
        if (is_numeric($value)) {
            return (int) $value === 1;
        }
        $value = strtolower(trim((string) $value));
        return in_array($value, ['1', 'true', 'yes', 'on'], true);
    }

    // ─── Field mapping helpers ────────────────────────────────────────────────

    protected function getFormFieldOptionsForMapping(): array
    {
        if (is_array($this->form_field_option_cache)) {
            return $this->form_field_option_cache;
        }

        $options = [];
        $meta    = [];
        $form_id = $this->resolveFormContextId();
        if ($form_id > 0) {
            $form = new \GriffinForms\Items\Form();
            if ($form->loadItem($form_id)) {
                $fields = $form->getFormFields(['id', 'name', 'field_type']);
                if (is_array($fields)) {
                    foreach ($fields as $field) {
                        $field_id = isset($field['id']) ? (string) $field['id'] : '';
                        if ($field_id === '') {
                            continue;
                        }

                        $label = trim((string) ($field['name'] ?? ''));
                        if ($label === '') {
                            $label = sprintf(__('Field #%d', 'griffinforms-form-builder'), (int) $field_id);
                        }

                        $type = isset($field['field_type']) ? strtolower((string) $field['field_type']) : '';
                        if ($type === 'payment' || $type === 'fileupload') {
                            continue;
                        }

                        $type_label = \GriffinForms\Includes\Fields\FieldRegistry::getInstance()->getLabel($type);
                        if ($type_label !== '') {
                            $label .= ' (' . $type_label . ')';
                        }

                        $options[$field_id] = $label;
                        $meta[$field_id]    = ['type' => $type, 'label' => $label];
                    }
                }
            }
        }

        $this->form_field_option_cache = $options;
        $this->form_field_meta_cache   = $meta;
        return $options;
    }

    protected function getEligibleFieldOptions(array $allowed_types): array
    {
        $options       = [];
        $all           = $this->getFormFieldOptionsForMapping();
        $allowed_types = array_map('strtolower', $allowed_types);

        foreach ($all as $field_id => $label) {
            $type = strtolower((string) ($this->form_field_meta_cache[$field_id]['type'] ?? ''));
            if (in_array($type, $allowed_types, true)) {
                $options[$field_id] = $label;
            }
        }

        return $options;
    }

    protected function getConsentFieldOptions(): array
    {
        return $this->getEligibleFieldOptions(['inputcheckbox', 'termsconditions']);
    }

    protected function normalizeFieldId($value): string
    {
        $value = trim((string) $value);
        if ($value === '') {
            return '';
        }
        if (!ctype_digit($value)) {
            $value = preg_replace('/[^0-9]/', '', $value);
        }
        return $value === '' ? '' : (string) absint($value);
    }

    protected function sanitizeMappedFieldId($value, array $valid_options, string $context_label): string
    {
        $normalized = $this->normalizeFieldId($value);
        if ($normalized === '') {
            return '';
        }

        if (!isset($valid_options[$normalized])) {
            $this->log->add(
                'warning',
                'form',
                $this->resolveFormContextId(),
                get_current_user_id(),
                sprintf('%s references an unsupported field ID (%s).', $context_label, $normalized),
                sprintf('Integration (%s)', $this->id)
            );
            return '';
        }

        return $normalized;
    }
}
