<?php

namespace PloverCRM\Modules\Integrations\Services;

/**
 * Webhook Service
 * 
 * Manages incoming webhook endpoints.
 * Each webhook has a unique token used in the URL for receiving data.
 */
class WebhookService
{
  const TABLE_NAME = 'plover_crm_webhooks';

  private const EXCLUDED_DISCOVERED_FIELD_KEYS = [
    'plover_crm_current_url',
    'plover_crm_referer',
    'plover_crm_user_ip',
    'plover_crm_user_agent',
  ];

  private const EXCLUDED_DISCOVERED_FIELD_PREFIXES = [
    'plover_crm_cookies',
  ];

  private const RECOMMENDED_FIELD_TOKENS = [
    'email',
    'emailaddress',
    'firstname',
    'first',
    'fname',
    'lastname',
    'last',
    'lname',
    'fullname',
    'name',
    'phone',
    'phonenumber',
    'mobile',
    'company',
    'companyname',
    'organization',
    'website',
    'url',
    'address',
    'addressline1',
    'address1',
    'addressline2',
    'address2',
    'city',
    'state',
    'province',
    'zip',
    'zipcode',
    'postalcode',
    'country',
    'jobtitle',
    'title',
    'dateofbirth',
    'dob',
    'birthday',
  ];

  /**
   * Ensure the webhooks table exists
   */
  public static function ensureTableExists(): void
  {
    global $wpdb;
    $table = $wpdb->prefix . self::TABLE_NAME;

    if ($wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table) {
      self::createTable();
    }
  }

  /**
   * Create the webhooks table
   */
  public static function createTable(): void
  {
    global $wpdb;
    $table = $wpdb->prefix . self::TABLE_NAME;
    $charset_collate = $wpdb->get_charset_collate();

    $sql = "CREATE TABLE $table (
      id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      name varchar(255) NOT NULL,
      token varchar(64) NOT NULL,
      last_payload longtext DEFAULT NULL,
      status varchar(20) NOT NULL DEFAULT 'active',
      created_at bigint(20) unsigned NOT NULL DEFAULT 0,
      updated_at bigint(20) unsigned NOT NULL DEFAULT 0,
      PRIMARY KEY (id),
      UNIQUE KEY token (token),
      KEY status (status)
    ) $charset_collate;";

    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    dbDelta($sql);
  }

  /**
   * Get all webhooks
   */
  public static function getAll(): array
  {
    global $wpdb;
    $table = $wpdb->prefix . self::TABLE_NAME;
    return $wpdb->get_results("SELECT * FROM $table ORDER BY id DESC", ARRAY_A) ?: [];
  }

  /**
   * Get a webhook by ID
   */
  public static function getById(int $id): ?array
  {
    global $wpdb;
    $table = $wpdb->prefix . self::TABLE_NAME;
    $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE id = %d", $id), ARRAY_A);
    return $row ?: null;
  }

  /**
   * Get a webhook by token
   */
  public static function getByToken(string $token): ?array
  {
    global $wpdb;
    $table = $wpdb->prefix . self::TABLE_NAME;
    $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE token = %s", $token), ARRAY_A);
    return $row ?: null;
  }

  /**
   * Create a new webhook
   */
  public static function create(string $name): ?array
  {
    global $wpdb;
    $table = $wpdb->prefix . self::TABLE_NAME;

    $token = self::generateToken();
    $now = time();

    $wpdb->insert($table, [
      'name' => sanitize_text_field($name),
      'token' => $token,
      'status' => 'active',
      'created_at' => $now,
      'updated_at' => $now,
    ]);

    $id = $wpdb->insert_id;
    if (!$id) {
      return null;
    }

    return self::getById($id);
  }

  /**
   * Update a webhook
   */
  public static function update(int $id, array $data): bool
  {
    global $wpdb;
    $table = $wpdb->prefix . self::TABLE_NAME;

    $allowed = ['name', 'status'];
    $updateData = [];

    foreach ($allowed as $key) {
      if (isset($data[$key])) {
        $updateData[$key] = sanitize_text_field($data[$key]);
      }
    }

    if (empty($updateData)) {
      return false;
    }

    $updateData['updated_at'] = time();

    return (bool) $wpdb->update($table, $updateData, ['id' => $id]);
  }

  /**
   * Delete a webhook
   */
  public static function delete(int $id): bool
  {
    global $wpdb;
    $table = $wpdb->prefix . self::TABLE_NAME;
    return (bool) $wpdb->delete($table, ['id' => $id], ['%d']);
  }

  /**
   * Delete a webhook and any webhook-backed mappings that depend on it.
   */
  public static function deleteWithMappings(int $id): bool
  {
    global $wpdb;
    $webhooksTable = $wpdb->prefix . self::TABLE_NAME;
    $dataSourcesTable = $wpdb->prefix . 'plover_crm_data_sources';
    $sourceId = (string) $id;

    $wpdb->query('START TRANSACTION');

    $deletedWebhook = $wpdb->delete($webhooksTable, ['id' => $id], ['%d']);
    if ($deletedWebhook === false) {
      $wpdb->query('ROLLBACK');
      return false;
    }

    $deletedMappings = $wpdb->delete(
      $dataSourcesTable,
      [
        'provider' => 'webhook',
        'source_id' => $sourceId,
      ],
      ['%s', '%s']
    );

    if ($deletedMappings === false) {
      $wpdb->query('ROLLBACK');
      return false;
    }

    $wpdb->query('COMMIT');
    return true;
  }

  /**
   * Store the last received payload for field discovery
   * Merges with previously discovered fields so no keys are lost
   */
  public static function storeLastPayload(int $id, array $payload): void
  {
    global $wpdb;
    $table = $wpdb->prefix . self::TABLE_NAME;

    // Get existing payload and merge keys
    $existing = self::getById($id);
    if ($existing && !empty($existing['last_payload'])) {
      $existingPayload = json_decode($existing['last_payload'], true);
      if (is_array($existingPayload)) {
        // Deep merge — new payload values take priority, but old keys are preserved
        $payload = self::deepMerge($existingPayload, $payload);
      }
    }

    $wpdb->update($table, [
      'last_payload' => json_encode($payload),
      'updated_at' => time(),
    ], ['id' => $id]);
  }

  /**
   * Deep merge two arrays — $b values override $a, but $a keys are preserved
   */
  private static function deepMerge(array $a, array $b): array
  {
    foreach ($b as $key => $value) {
      if (is_array($value) && isset($a[$key]) && is_array($a[$key]) && !self::isIndexedArray($value)) {
        $a[$key] = self::deepMerge($a[$key], $value);
      } else {
        $a[$key] = $value;
      }
    }
    return $a;
  }

  /**
   * Get the flattened field keys from the last payload
   */
  public static function getDiscoveredFields(int $id): array
  {
    $webhook = self::getById($id);
    if (!$webhook || empty($webhook['last_payload'])) {
      return [];
    }

    $payload = json_decode($webhook['last_payload'], true);
    if (!is_array($payload)) {
      return [];
    }

    $fields = [];
    self::flattenKeys($payload, '', $fields);
    usort($fields, [self::class, 'sortDiscoveredFields']);
    return $fields;
  }

  /**
   * Recursively flatten nested array keys into dot notation
   */
  private static function flattenKeys(array $data, string $prefix, array &$fields): void
  {
    foreach ($data as $key => $value) {
      $fullKey = $prefix === '' ? (string) $key : $prefix . '.' . $key;

      if (self::shouldExcludeDiscoveredField($fullKey)) {
        continue;
      }

      if (is_array($value) && !self::isIndexedArray($value)) {
        // Nested associative array — recurse
        self::flattenKeys($value, $fullKey, $fields);
      } else {
        $fields[] = [
          'key' => $fullKey,
          'label' => self::humanizeDiscoveredFieldLabel($fullKey),
          'type' => self::guessFieldType($value),
          'group' => self::resolveDiscoveredFieldGroup($fullKey),
          'recommended' => self::isRecommendedDiscoveredField($fullKey),
        ];
      }
    }
  }

  private static function shouldExcludeDiscoveredField(string $key): bool
  {
    if (in_array($key, self::EXCLUDED_DISCOVERED_FIELD_KEYS, true)) {
      return true;
    }

    foreach (self::EXCLUDED_DISCOVERED_FIELD_PREFIXES as $prefix) {
      if (str_starts_with($key, $prefix)) {
        return true;
      }
    }

    return false;
  }

  private static function resolveDiscoveredFieldGroup(string $key): string
  {
    if (str_starts_with($key, 'plover_crm_')) {
      return 'Visitor Metadata';
    }

    if (self::isRecommendedDiscoveredField($key)) {
      return 'Contact Fields';
    }

    return str_contains($key, '.')
      ? 'Payload Fields'
      : 'Top-level Fields';
  }

  private static function isRecommendedDiscoveredField(string $key): bool
  {
    $normalizedCandidates = self::getNormalizedFieldCandidates($key);

    foreach ($normalizedCandidates as $candidate) {
      if (in_array($candidate, self::RECOMMENDED_FIELD_TOKENS, true)) {
        return true;
      }
    }

    return false;
  }

  private static function getNormalizedFieldCandidates(string $key): array
  {
    $segments = explode('.', $key);
    $leaf = end($segments) ?: $key;
    $candidates = [
      self::normalizeFieldToken($key),
      self::normalizeFieldToken($leaf),
    ];

    return array_values(array_unique(array_filter($candidates)));
  }

  private static function normalizeFieldToken(string $value): string
  {
    return strtolower((string) preg_replace('/[^a-z0-9]/', '', $value));
  }

  private static function humanizeDiscoveredFieldLabel(string $key): string
  {
    $segments = array_filter(explode('.', $key), static function ($segment) {
      return $segment !== '';
    });

    $parts = array_map(static function ($segment) {
      $segment = str_replace(['_', '-'], ' ', (string) $segment);
      return ucwords(trim($segment));
    }, $segments);

    return trim(implode(' ', $parts));
  }

  private static function sortDiscoveredFields(array $left, array $right): int
  {
    $leftRecommended = !empty($left['recommended']);
    $rightRecommended = !empty($right['recommended']);

    if ($leftRecommended !== $rightRecommended) {
      return $leftRecommended ? -1 : 1;
    }

    $leftWeight = self::getDiscoveredFieldGroupWeight((string) ($left['group'] ?? ''));
    $rightWeight = self::getDiscoveredFieldGroupWeight((string) ($right['group'] ?? ''));

    if ($leftWeight !== $rightWeight) {
      return $leftWeight <=> $rightWeight;
    }

    return strcasecmp((string) ($left['label'] ?? ''), (string) ($right['label'] ?? ''));
  }

  private static function getDiscoveredFieldGroupWeight(string $group): int
  {
    return match ($group) {
      'Contact Fields' => 0,
      'Top-level Fields' => 1,
      'Payload Fields' => 2,
      'Visitor Metadata' => 3,
      default => 4,
    };
  }

  /**
   * Check if an array is numerically indexed (list) vs associative (object)
   */
  private static function isIndexedArray(array $arr): bool
  {
    return array_keys($arr) === range(0, count($arr) - 1);
  }

  /**
   * Guess field type from value
   */
  private static function guessFieldType($value): string
  {
    if (is_bool($value)) return 'checkbox';
    if (is_numeric($value)) return 'number';
    if (is_string($value) && filter_var($value, FILTER_VALIDATE_EMAIL)) return 'email';
    if (is_string($value) && filter_var($value, FILTER_VALIDATE_URL)) return 'url';
    return 'text';
  }

  /**
   * Generate a unique webhook token
   */
  private static function generateToken(): string
  {
    return bin2hex(random_bytes(16));
  }

  /**
   * Get the public webhook URL for a token
   */
  public static function getWebhookUrl(string $token): string
  {
    return rest_url('plover-crm/v1/webhook/incoming/' . $token);
  }
}
