<?php
/**
 * Forms — public API.
 *
 * Static methods so other plugins can reach in without instantiating
 * the Forms bootstrap class:
 *
 *   FormsAPI::getForm($slug)
 *   FormsAPI::parseFieldDsl($text)
 *   FormsAPI::renderField($field, $value, $error)
 *   FormsAPI::validateSubmission($form, $input)
 *   FormsAPI::generateRef()
 *   FormsAPI::slugify($title, $excludeId = null)
 *   FormsAPI::dispatchWebhooks($formId, $submissionId, $payload)
 *
 * Field DSL (one field per line, pipe-separated):
 *   type|name|label|required|placeholder|options
 *
 * Examples:
 *   text|full_name|Your name|required
 *   email|email|Email|required|you@example.com
 *   tel|phone|Phone
 *   textarea|message|Tell us more|required
 *   select|reason|Reason for contact||Choose…|sales,support,other
 *   radio|priority|Priority|required||low,normal,high
 *   checkbox|consent|I agree to be contacted|required
 *   number|budget|Budget (USD)|required||1000
 *   date|when|Preferred date
 *
 * Lines starting with `#` are comments. Blank lines are ignored.
 *
 * Supported types (this first cut): text, email, tel, url, number,
 * date, textarea, select, radio, checkbox.
 * Deferred to follow-up sessions: file, signature, multi-step,
 * conditional logic, payment.
 */

class FormsAPI {

    const SUPPORTED_TYPES = [
        'text', 'email', 'tel', 'url', 'number', 'date',
        'textarea', 'select', 'radio', 'checkbox', 'file',
    ];

    /** Webhook signature: HMAC-SHA256 of the raw JSON body, keyed by per-form secret. */
    const WEBHOOK_SIGNATURE_HEADER = 'X-Slate-Signature';

    /** True once per request after we've confirmed the schema exists. */
    private static bool $schemaChecked = false;

    /**
     * Lazily create the forms_* tables if they don't already exist.
     * Defensive: if install.sql didn't run during plugin activation
     * (some hosts swallow CREATE TABLE failures), this ensures every
     * admin page and the public router can still operate.
     */
    public static function ensureSchema(): void {
        if (self::$schemaChecked) return;
        self::$schemaChecked = true;
        try {
            $pdo = Database::get();
            $pdo->exec(
                "CREATE TABLE IF NOT EXISTS `forms_definitions` (
                    `id`               INT UNSIGNED NOT NULL AUTO_INCREMENT,
                    `tenant_id`        INT UNSIGNED NOT NULL DEFAULT 1,
                    `slug`             VARCHAR(80) NOT NULL,
                    `title`            VARCHAR(200) NOT NULL,
                    `description`      TEXT NULL,
                    `fields_json`      LONGTEXT NOT NULL,
                    `submit_label`     VARCHAR(80) NOT NULL DEFAULT 'Submit',
                    `success_message`  TEXT NULL,
                    `redirect_url`     VARCHAR(500) NULL,
                    `notify_email`     VARCHAR(200) NULL,
                    `confirm_submitter` TINYINT(1) NOT NULL DEFAULT 0,
                    `confirm_subject`  VARCHAR(200) NULL,
                    `confirm_body`     TEXT NULL,
                    `status`           ENUM('draft','published','archived') NOT NULL DEFAULT 'draft',
                    `submission_limit` INT UNSIGNED NULL,
                    `created_at`       DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                    `updated_at`       DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                    PRIMARY KEY (`id`),
                    UNIQUE KEY `tenant_slug` (`tenant_id`, `slug`),
                    KEY `tenant_status` (`tenant_id`, `status`)
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
            );
            $pdo->exec(
                "CREATE TABLE IF NOT EXISTS `forms_submissions` (
                    `id`              INT UNSIGNED NOT NULL AUTO_INCREMENT,
                    `tenant_id`       INT UNSIGNED NOT NULL DEFAULT 1,
                    `form_id`         INT UNSIGNED NOT NULL,
                    `ref`             VARCHAR(32) NOT NULL,
                    `data_json`       LONGTEXT NOT NULL,
                    `submitter_email` VARCHAR(200) NULL,
                    `ip`              VARCHAR(60) NULL,
                    `user_agent`      VARCHAR(255) NULL,
                    `read_at`         DATETIME NULL,
                    `email_sent`      TINYINT(1) NOT NULL DEFAULT 0,
                    `email_error`     TEXT NULL,
                    `created_at`      DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                    PRIMARY KEY (`id`),
                    UNIQUE KEY `tenant_ref` (`tenant_id`, `ref`),
                    KEY `tenant_form` (`tenant_id`, `form_id`),
                    KEY `tenant_read` (`tenant_id`, `read_at`),
                    KEY `tenant_created` (`tenant_id`, `created_at`)
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
            );
            $pdo->exec(
                "CREATE TABLE IF NOT EXISTS `forms_webhooks` (
                    `id`         INT UNSIGNED NOT NULL AUTO_INCREMENT,
                    `tenant_id`  INT UNSIGNED NOT NULL DEFAULT 1,
                    `form_id`    INT UNSIGNED NOT NULL,
                    `url`        VARCHAR(500) NOT NULL,
                    `is_active`  TINYINT(1) NOT NULL DEFAULT 1,
                    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                    PRIMARY KEY (`id`),
                    KEY `tenant_form` (`tenant_id`, `form_id`)
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
            );
            $pdo->exec(
                "CREATE TABLE IF NOT EXISTS `forms_webhook_log` (
                    `id`            INT UNSIGNED NOT NULL AUTO_INCREMENT,
                    `tenant_id`     INT UNSIGNED NOT NULL DEFAULT 1,
                    `webhook_id`    INT UNSIGNED NOT NULL,
                    `submission_id` INT UNSIGNED NULL,
                    `status_code`   INT NULL,
                    `response_body` TEXT NULL,
                    `error`         TEXT NULL,
                    `created_at`    DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                    PRIMARY KEY (`id`),
                    KEY `tenant_webhook` (`tenant_id`, `webhook_id`),
                    KEY `tenant_created` (`tenant_id`, `created_at`)
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
            );
        } catch (\Throwable $e) {
            if (function_exists('slate_log')) {
                slate_log('FormsAPI::ensureSchema failed: ' . $e->getMessage(), 'error');
            }
        }
    }

    /** Load a form by slug (tenant-scoped). Decodes fields_json. */
    public static function getForm(string $slug): ?array {
        $row = Database::row(
            "SELECT * FROM forms_definitions WHERE tenant_id = ? AND slug = ?",
            [current_tenant_id(), $slug]
        );
        if (!$row) return null;
        $row['fields'] = self::decodeFields($row['fields_json'] ?? '');
        return $row;
    }

    public static function getFormById(int $id): ?array {
        $row = Database::row(
            "SELECT * FROM forms_definitions WHERE id = ? AND tenant_id = ?",
            [$id, current_tenant_id()]
        );
        if (!$row) return null;
        $row['fields'] = self::decodeFields($row['fields_json'] ?? '');
        return $row;
    }

    /**
     * Parse the field DSL into a normalised array of field defs.
     * Returns ['fields' => [...], 'errors' => [...]] so callers can
     * show line-level parse errors in the editor.
     */
    public static function parseFieldDsl(string $text): array {
        $out    = [];
        $errors = [];
        $usedNames = [];
        $lineNum = 0;
        foreach (preg_split('/\r\n|\n|\r/', $text) as $raw) {
            $lineNum++;
            $line = trim($raw);
            if ($line === '' || str_starts_with($line, '#')) continue;

            $parts = array_map('trim', explode('|', $line));
            $type        = strtolower($parts[0] ?? '');
            $name        = $parts[1] ?? '';
            $label       = $parts[2] ?? '';
            $required    = strtolower($parts[3] ?? '') === 'required';
            $placeholder = $parts[4] ?? '';
            $optionsRaw  = $parts[5] ?? '';

            if (!in_array($type, self::SUPPORTED_TYPES, true)) {
                $errors[] = "Line {$lineNum}: unsupported field type '" . $type . "'.";
                continue;
            }
            if (!preg_match('/^[a-z][a-z0-9_]{0,49}$/i', $name)) {
                $errors[] = "Line {$lineNum}: name must start with a letter and contain only letters/digits/underscores.";
                continue;
            }
            $name = strtolower($name);
            if (isset($usedNames[$name])) {
                $errors[] = "Line {$lineNum}: duplicate field name '{$name}'.";
                continue;
            }
            $usedNames[$name] = true;

            $field = [
                'type'     => $type,
                'name'     => $name,
                'label'    => $label !== '' ? $label : ucwords(str_replace('_', ' ', $name)),
                'required' => $required,
            ];
            if ($placeholder !== '') $field['placeholder'] = $placeholder;

            if (in_array($type, ['select', 'radio'], true)) {
                $opts = $optionsRaw !== ''
                      ? array_values(array_filter(array_map('trim', explode(',', $optionsRaw)), fn($s) => $s !== ''))
                      : [];
                if (empty($opts)) {
                    $errors[] = "Line {$lineNum}: {$type} field '{$name}' needs an options list (comma-separated).";
                    continue;
                }
                $field['options'] = $opts;
            }

            $out[] = $field;
        }
        return ['fields' => $out, 'errors' => $errors];
    }

    /**
     * Serialise the parsed field list back to DSL text — used by the
     * edit page to round-trip safely after a save.
     */
    public static function fieldsToDsl(array $fields): string {
        $lines = [];
        foreach ($fields as $f) {
            $parts = [
                $f['type'] ?? 'text',
                $f['name'] ?? '',
                $f['label'] ?? '',
                !empty($f['required']) ? 'required' : '',
                $f['placeholder'] ?? '',
                isset($f['options']) ? implode(',', (array)$f['options']) : '',
            ];
            // Trim trailing empties for readability.
            while (count($parts) > 3 && end($parts) === '') array_pop($parts);
            $lines[] = implode('|', $parts);
        }
        return implode("\n", $lines);
    }

    private static function decodeFields(string $json): array {
        $arr = json_decode($json, true);
        return is_array($arr) ? $arr : [];
    }

    /**
     * Validate a $_POST-style input array against a form's fields.
     * Returns ['ok'=>bool, 'errors'=>['name'=>'message'], 'data'=>[...]].
     */
    public static function validateSubmission(array $form, array $input): array {
        $errors = [];
        $data   = [];

        foreach (($form['fields'] ?? []) as $f) {
            $name = $f['name'] ?? '';
            if ($name === '') continue;
            $type     = $f['type'] ?? 'text';
            $required = !empty($f['required']);

            $raw = $input[$name] ?? null;

            // Checkboxes: a single checkbox = presence boolean; missing key = unchecked.
            if ($type === 'checkbox') {
                $checked = !empty($raw);
                if ($required && !$checked) {
                    $errors[$name] = ($f['label'] ?? $name) . ' is required.';
                }
                $data[$name] = $checked;
                continue;
            }

            $value = is_string($raw) ? trim($raw) : '';
            if ($value === '' && $raw !== null && !is_string($raw)) {
                // Defensive: arrays etc. aren't accepted for non-checkbox.
                $value = '';
            }

            if ($value === '') {
                if ($required) {
                    $errors[$name] = ($f['label'] ?? $name) . ' is required.';
                }
                $data[$name] = '';
                continue;
            }

            switch ($type) {
                case 'email':
                    if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
                        $errors[$name] = 'Please enter a valid email address.';
                    }
                    break;
                case 'url':
                    if (!filter_var($value, FILTER_VALIDATE_URL)) {
                        $errors[$name] = 'Please enter a valid URL.';
                    }
                    break;
                case 'number':
                    if (!is_numeric($value)) {
                        $errors[$name] = 'Please enter a number.';
                    }
                    break;
                case 'date':
                    // Accept YYYY-MM-DD; let the browser do the heavy work
                    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
                        $errors[$name] = 'Please enter a date.';
                    }
                    break;
                case 'select':
                case 'radio':
                    $opts = (array)($f['options'] ?? []);
                    if (!in_array($value, $opts, true)) {
                        $errors[$name] = 'Please choose one of the listed options.';
                    }
                    break;
                case 'tel':
                    if (!preg_match('/^[0-9+()\-.\s]{4,40}$/', $value)) {
                        $errors[$name] = 'Please enter a valid phone number.';
                    }
                    break;
                case 'textarea':
                    if (mb_strlen($value) > 8000) {
                        $errors[$name] = 'Please keep this under 8000 characters.';
                    }
                    break;
                case 'file':
                    // File validation happens out-of-band — see
                    // FormsAPI::handleFileUploads() called from the
                    // public router. The DSL only records "file is
                    // required"; the actual file lives in $_FILES.
                    $value = ''; // placeholder; real path set later
                    break;
                case 'text':
                default:
                    if (mb_strlen($value) > 500) {
                        $errors[$name] = 'Please keep this under 500 characters.';
                    }
                    break;
            }

            $data[$name] = $value;
        }

        return [
            'ok'     => empty($errors),
            'errors' => $errors,
            'data'   => $data,
        ];
    }

    /**
     * Render one field as HTML for the public form. Uses the shared
     * design-system classes (.field / .field-label / etc.) so the
     * markup is identical between admin previews and the live form.
     */
    public static function renderField(array $field, $value = null, ?string $error = null): string {
        $name        = $field['name'] ?? '';
        $type        = $field['type'] ?? 'text';
        $label       = $field['label'] ?? $name;
        $required    = !empty($field['required']);
        $placeholder = $field['placeholder'] ?? '';

        if ($name === '') return '';

        $id   = 'f_' . preg_replace('/[^a-z0-9_]/i', '', $name);
        $req  = $required ? ' required' : '';
        $star = $required ? ' <span class="field-required">*</span>' : '';

        $valueAttr = is_string($value) ? e($value) : '';
        $checked   = (bool)$value;

        ob_start();
        echo '<div class="field">';

        if ($type === 'checkbox') {
            echo '<label style="display:flex;align-items:flex-start;gap:8px;cursor:pointer;">';
            echo '<input type="checkbox" id="' . e($id) . '" name="' . e($name) . '" value="1"'
                . ($checked ? ' checked' : '') . $req . ' style="margin-top:3px;">';
            echo '<span class="field-label" style="margin:0;">' . e($label) . $star . '</span>';
            echo '</label>';
        } else {
            echo '<label class="field-label" for="' . e($id) . '">' . e($label) . $star . '</label>';

            $phAttr = $placeholder !== '' ? ' placeholder="' . e($placeholder) . '"' : '';

            switch ($type) {
                case 'textarea':
                    echo '<textarea id="' . e($id) . '" name="' . e($name) . '"' . $req . $phAttr . '>'
                       . $valueAttr . '</textarea>';
                    break;

                case 'select':
                    echo '<select id="' . e($id) . '" name="' . e($name) . '"' . $req . '>';
                    if ($placeholder !== '') {
                        echo '<option value="">' . e($placeholder) . '</option>';
                    } elseif (!$required) {
                        echo '<option value="">—</option>';
                    }
                    foreach ((array)($field['options'] ?? []) as $opt) {
                        $sel = (is_string($value) && $value === $opt) ? ' selected' : '';
                        echo '<option value="' . e($opt) . '"' . $sel . '>' . e($opt) . '</option>';
                    }
                    echo '</select>';
                    break;

                case 'file':
                    echo '<input type="file" id="' . e($id) . '" name="' . e($name) . '"'
                       . $req . ' accept="image/*,application/pdf,.doc,.docx,.txt">';
                    echo '<div class="field-hint">Max 10 MB. Images, PDF, or document files.</div>';
                    break;

                case 'radio':
                    echo '<div class="form-radio-group" style="display:flex;flex-direction:column;gap:6px;">';
                    foreach ((array)($field['options'] ?? []) as $i => $opt) {
                        $optId = $id . '_' . $i;
                        $sel = (is_string($value) && $value === $opt) ? ' checked' : '';
                        echo '<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400;">';
                        echo '<input type="radio" id="' . e($optId) . '" name="' . e($name) . '" value="' . e($opt) . '"'
                           . $sel . ($required ? ' required' : '') . '>';
                        echo '<span>' . e($opt) . '</span>';
                        echo '</label>';
                    }
                    echo '</div>';
                    break;

                default:
                    $inputType = $type;
                    if (!in_array($type, ['text','email','tel','url','number','date'], true)) {
                        $inputType = 'text';
                    }
                    echo '<input type="' . e($inputType) . '" id="' . e($id) . '" name="' . e($name) . '"'
                       . ($valueAttr !== '' ? ' value="' . $valueAttr . '"' : '')
                       . $req . $phAttr . '>';
                    break;
            }
        }

        if ($error !== null && $error !== '') {
            echo '<div class="field-error">' . e($error) . '</div>';
        }

        echo '</div>';
        return (string) ob_get_clean();
    }

    /** Pretty submission reference: SUB-XXXXXXXX (8 hex chars). */
    public static function generateRef(): string {
        return 'SUB-' . strtoupper(bin2hex(random_bytes(4)));
    }

    /**
     * Return a slug unique within the tenant's forms_definitions
     * table. $excludeId lets the edit page keep its own slug.
     */
    public static function slugify(string $title, ?int $excludeId = null): string {
        $base = strtolower(trim(preg_replace('/[^a-z0-9]+/i', '-', $title), '-'));
        if ($base === '') $base = 'form';
        if (mb_strlen($base) > 60) $base = substr($base, 0, 60);

        $tid = current_tenant_id();
        $slug = $base;
        $i = 2;
        while (true) {
            $clash = Database::row(
                "SELECT id FROM forms_definitions
                  WHERE tenant_id = ? AND slug = ?" . ($excludeId ? " AND id <> ?" : ""),
                $excludeId ? [$tid, $slug, $excludeId] : [$tid, $slug]
            );
            if (!$clash) return $slug;
            $slug = $base . '-' . $i++;
            if ($i > 999) return $base . '-' . bin2hex(random_bytes(3));
        }
    }

    /**
     * Process file fields after a successful validateSubmission.
     * Writes each file via Uploads::handle into uploads/forms/.
     * Updates $data in place (returns merged data + errors).
     */
    public static function handleFileUploads(array $form, array $data): array {
        $errors = [];
        foreach (($form['fields'] ?? []) as $f) {
            if (($f['type'] ?? '') !== 'file') continue;
            $name     = $f['name']  ?? '';
            $required = !empty($f['required']);
            if ($name === '') continue;

            $fileMeta = $_FILES[$name] ?? null;
            if (!$fileMeta || (int)($fileMeta['error'] ?? 4) === UPLOAD_ERR_NO_FILE) {
                if ($required) {
                    $errors[$name] = ($f['label'] ?? $name) . ' is required.';
                }
                continue;
            }

            $result = Uploads::handle($name, 'forms', [
                'max_bytes'     => 10 * 1024 * 1024,
                'allowed_exts'  => ['jpg','jpeg','png','gif','webp','pdf','doc','docx','txt'],
            ]);

            if (!empty($result['ok'])) {
                $data[$name] = [
                    'path'     => $result['path'],
                    'original' => $result['original'] ?? '',
                    'size'     => $result['size']     ?? 0,
                    'mime'     => $result['mime']     ?? '',
                ];
            } else {
                $errors[$name] = $result['error'] ?? 'Upload failed.';
            }
        }
        return ['data' => $data, 'errors' => $errors];
    }

    /**
     * POST the submission payload to every active webhook for the
     * form. Best-effort: failures are logged to forms_webhook_log
     * but don't fail the submit.
     *
     * Adds an HMAC-SHA256 signature header so receivers can verify
     * the payload. The shared secret is APP_SECRET (set in .env).
     */
    public static function dispatchWebhooks(int $formId, int $submissionId, array $payload): void {
        $tid = current_tenant_id();
        $hooks = Database::rows(
            "SELECT * FROM forms_webhooks
              WHERE tenant_id = ? AND form_id = ? AND is_active = 1",
            [$tid, $formId]
        );
        if (!$hooks) return;

        $body      = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        $secret    = defined('APP_SECRET') ? (string) APP_SECRET : '';
        $signature = $secret !== '' ? hash_hmac('sha256', $body, $secret) : '';

        foreach ($hooks as $hook) {
            $statusCode = null;
            $response   = null;
            $error      = null;

            try {
                $ch = curl_init($hook['url']);
                curl_setopt_array($ch, [
                    CURLOPT_RETURNTRANSFER => true,
                    CURLOPT_POST           => true,
                    CURLOPT_POSTFIELDS     => $body,
                    CURLOPT_HTTPHEADER     => array_filter([
                        'Content-Type: application/json',
                        'User-Agent: Slate-Forms/0.1',
                        'X-Slate-Form: ' . $formId,
                        'X-Slate-Submission: ' . $submissionId,
                        $signature !== '' ? self::WEBHOOK_SIGNATURE_HEADER . ': sha256=' . $signature : null,
                    ]),
                    CURLOPT_TIMEOUT        => 10,
                    CURLOPT_CONNECTTIMEOUT => 5,
                    CURLOPT_FOLLOWLOCATION => false,
                    CURLOPT_SSL_VERIFYPEER => true,
                    CURLOPT_SSL_VERIFYHOST => 2,
                ]);
                $response = curl_exec($ch);
                if ($response === false) {
                    $error = curl_error($ch);
                } else {
                    $statusCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
                    // Cap body size we log to keep the log table sane.
                    if (is_string($response) && strlen($response) > 4000) {
                        $response = substr($response, 0, 4000) . '...[truncated]';
                    }
                }
                curl_close($ch);
            } catch (\Throwable $e) {
                $error = $e->getMessage();
            }

            try {
                Database::insert('forms_webhook_log', [
                    'tenant_id'     => $tid,
                    'webhook_id'    => (int)$hook['id'],
                    'submission_id' => $submissionId,
                    'status_code'   => $statusCode,
                    'response_body' => $response,
                    'error'         => $error,
                ]);
            } catch (\Throwable $e) {
                if (function_exists('slate_log')) {
                    slate_log('Forms: webhook log insert failed: ' . $e->getMessage(), 'error');
                }
            }
        }
    }

    /** Best-effort client IP, respecting common proxy headers. */
    public static function clientIp(): string {
        $candidates = ['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR'];
        foreach ($candidates as $key) {
            if (!empty($_SERVER[$key])) {
                $val = (string)$_SERVER[$key];
                if (str_contains($val, ',')) {
                    $val = trim(explode(',', $val)[0]);
                }
                if (filter_var($val, FILTER_VALIDATE_IP)) return $val;
            }
        }
        return '';
    }
}
