<?php

namespace GriffinForms\Includes\Pipelines;

use GriffinForms\Config;
use GriffinForms\Includes\Helpers\Compliance;
use GriffinForms\Traits\Singleton;

class SubmissionHistoryManager
{
    use Singleton;

    protected $config;
    protected $wpdb;
    protected $submission_table;
    protected $revision_table;
    protected $log;

    protected function setConfig(): void
    {
        $this->config = Config::getInstance();
    }

    protected function setWpdb(): void
    {
        global $wpdb;
        $this->wpdb = $wpdb;
    }

    protected function setLog(): void
    {
        $this->log = \GriffinForms\Log::getInstance();
    }

    protected function setSubmissiontable(): void
    {
        $this->submission_table = $this->config->getTable('submission');
    }

    protected function setRevisiontable(): void
    {
        $this->revision_table = $this->config->getTable('submission_revision');
    }

    public function getRevisionTableName(): string
    {
        return (string) $this->revision_table;
    }

    public function getSubmissionTableName(): string
    {
        return (string) $this->submission_table;
    }

    public function getRevisions(int $submission_id, int $limit = 50): array
    {
        if ($submission_id <= 0) {
            return [];
        }

        $limit = max(1, $limit);
        $rows = $this->wpdb->get_results(
            $this->wpdb->prepare(
                "SELECT * FROM %i WHERE submission_id = %d ORDER BY revision_number DESC, id DESC LIMIT %d",
                $this->revision_table,
                $submission_id,
                $limit
            ),
            ARRAY_A
        );

        if (!is_array($rows)) {
            return [];
        }

        $revisions = [];
        foreach ($rows as $row) {
            $revisions[] = $this->normalizeRevisionRow($row);
        }

        return $revisions;
    }

    public function getRevisionByEventUid(int $submission_id, string $event_uid): ?array
    {
        $event_uid = trim($event_uid);
        if ($submission_id <= 0 || $event_uid === '') {
            return null;
        }

        $row = $this->wpdb->get_row(
            $this->wpdb->prepare(
                "SELECT * FROM %i WHERE submission_id = %d AND event_uid = %s LIMIT 1",
                $this->revision_table,
                $submission_id,
                $event_uid
            ),
            ARRAY_A
        );

        if (!is_array($row)) {
            return null;
        }

        return $this->normalizeRevisionRow($row);
    }

    public function recordRevision(
        int $submission_id,
        int $form_id,
        array $snapshot,
        string $event_type,
        array $context = []
    ): array {
        if ($submission_id <= 0 || $form_id <= 0) {
            return [];
        }

        $event_type = sanitize_key($event_type);
        if (!in_array($event_type, array('create', 'edit', 'restore'), true)) {
            $event_type = 'edit';
        }

        $actor_user_id = isset($context['actor_user_id']) ? absint($context['actor_user_id']) : get_current_user_id();
        $actor_label = isset($context['actor_label']) ? sanitize_text_field((string) $context['actor_label']) : '';
        if ($actor_label === '') {
            $actor = $actor_user_id > 0 ? get_user_by('id', $actor_user_id) : null;
            $actor_label = $actor && $actor->exists() ? (string) $actor->display_name : __('Unknown user', 'griffinforms-form-builder');
        }

        $previous_snapshot = isset($context['previous_snapshot']) && is_array($context['previous_snapshot']) ? $context['previous_snapshot'] : [];
        $delta = $this->buildDelta($previous_snapshot, $snapshot);
        $delta_json = !empty($delta) ? wp_json_encode($delta) : '';
        $snapshot_json = Compliance::canonicalJson($snapshot);
        $snapshot_hash = Compliance::hashSubmission($snapshot);
        $revision_number = $this->getNextRevisionNumber($submission_id);
        $event_uid = isset($context['event_uid']) && is_string($context['event_uid']) && $context['event_uid'] !== ''
            ? sanitize_text_field($context['event_uid'])
            : wp_generate_uuid4();
        $created = current_time('mysql', true);
        $restored_from_revision_id = isset($context['restored_from_revision_id']) ? absint($context['restored_from_revision_id']) : 0;

        $insert = $this->wpdb->insert(
            $this->revision_table,
            array(
                'event_uid' => $event_uid,
                'submission_id' => $submission_id,
                'form_id' => $form_id,
                'revision_number' => $revision_number,
                'event_type' => $event_type,
                'snapshot_json' => $snapshot_json,
                'snapshot_hash' => $snapshot_hash,
                'delta_json' => $delta_json,
                'actor_user_id' => $actor_user_id,
                'actor_label' => $actor_label,
                'restored_from_revision_id' => $restored_from_revision_id,
                'created' => $created,
            ),
            array('%s', '%d', '%d', '%d', '%s', '%s', '%s', '%s', '%d', '%s', '%d', '%s')
        );

        if ($insert === false) {
            return [];
        }

        $revision_id = (int) $this->wpdb->insert_id;
        $row = $this->getRevisionById($revision_id);
        if (!empty($context['update_pointer']) && $revision_id > 0) {
            $this->setCurrentRevisionId($submission_id, $revision_id);
        }

        return is_array($row) ? $row : array(
            'id' => $revision_id,
            'event_uid' => $event_uid,
            'submission_id' => $submission_id,
            'form_id' => $form_id,
            'revision_number' => $revision_number,
            'event_type' => $event_type,
            'snapshot_json' => $snapshot_json,
            'snapshot_hash' => $snapshot_hash,
            'delta_json' => $delta_json,
            'delta' => $delta,
            'actor_user_id' => $actor_user_id,
            'actor_label' => $actor_label,
            'restored_from_revision_id' => $restored_from_revision_id,
            'created' => $created,
        );
    }

    public function restoreRevision(int $submission_id, string $event_uid, array $context = []): array
    {
        $submission_id = absint($submission_id);
        $event_uid = sanitize_text_field($event_uid);
        if ($submission_id <= 0 || $event_uid === '') {
            return array(
                'success' => false,
                'message' => __('Invalid submission revision request.', 'griffinforms-form-builder'),
            );
        }

        $submission = $this->wpdb->get_row(
            $this->wpdb->prepare(
                "SELECT * FROM %i WHERE id = %d LIMIT 1",
                $this->submission_table,
                $submission_id
            ),
            ARRAY_A
        );
        if (!is_array($submission)) {
            return array(
                'success' => false,
                'message' => __('Submission not found.', 'griffinforms-form-builder'),
            );
        }

        $revision = $this->getRevisionByEventUid($submission_id, $event_uid);
        if (empty($revision) || !is_array($revision)) {
            return array(
                'success' => false,
                'message' => __('Revision not found.', 'griffinforms-form-builder'),
            );
        }

        $snapshot = $this->decodeSnapshot($revision['snapshot_json'] ?? []);
        if (empty($snapshot) || !is_array($snapshot)) {
            return array(
                'success' => false,
                'message' => __('Revision snapshot is not available.', 'griffinforms-form-builder'),
            );
        }

        $form_id = isset($submission['form_id']) ? (int) $submission['form_id'] : 0;
        if ($form_id <= 0) {
            $form_id = isset($revision['form_id']) ? (int) $revision['form_id'] : 0;
        }
        if ($form_id <= 0) {
            return array(
                'success' => false,
                'message' => __('Submission form could not be resolved.', 'griffinforms-form-builder'),
            );
        }

        $previous_snapshot = $this->decodeSnapshot($submission['submission'] ?? []);
        $revision_context = array(
            'actor_user_id' => isset($context['actor_user_id']) ? absint($context['actor_user_id']) : get_current_user_id(),
            'actor_label' => isset($context['actor_label']) ? sanitize_text_field((string) $context['actor_label']) : '',
            'previous_snapshot' => $previous_snapshot,
            'restored_from_revision_id' => (int) ($revision['id'] ?? 0),
            'update_pointer' => false,
        );

        $new_revision = $this->recordRevision($submission_id, $form_id, $snapshot, 'restore', $revision_context);
        if (empty($new_revision) || !is_array($new_revision)) {
            return array(
                'success' => false,
                'message' => __('Failed to record the rollback revision.', 'griffinforms-form-builder'),
            );
        }

        $update = array(
            'submission' => maybe_serialize($snapshot),
            'submission_version' => (int) ($submission['submission_version'] ?? 0) + 1,
            'submission_edit_count' => (int) ($submission['submission_edit_count'] ?? 0) + 1,
            'last_editor' => isset($revision_context['actor_user_id']) ? (int) $revision_context['actor_user_id'] : get_current_user_id(),
            'last_edited' => current_time('mysql', false),
            'current_revision_id' => (int) ($new_revision['id'] ?? 0),
        );

        $hash = '';
        if ($this->isComplianceEnabled($form_id)) {
            $hash = Compliance::hashSubmission($snapshot);
            $update['submission_hash'] = $hash;
        }

        $update_formats = array('%s', '%d', '%d', '%d', '%s', '%d');
        if ($hash !== '') {
            $update_formats[] = '%s';
        }

        $updated = $this->wpdb->update(
            $this->submission_table,
            $update,
            array('id' => $submission_id),
            $update_formats,
            array('%d')
        );

        if ($updated === false) {
            return array(
                'success' => false,
                'message' => __('Rollback could not update the submission record.', 'griffinforms-form-builder'),
            );
        }

        $this->logRestoreEvent($submission_id, (int) ($new_revision['revision_number'] ?? 0), true);

        return array(
            'success' => true,
            'message' => __('Submission restored.', 'griffinforms-form-builder'),
            'revision' => $new_revision,
            'snapshot' => $snapshot,
            'submission_id' => $submission_id,
            'form_id' => $form_id,
        );
    }

    public function backfillRevisionHistory(): void
    {
        $flag = get_option('griffinforms_submission_history_backfilled');
        if (!empty($flag)) {
            return;
        }

        if (!$this->revisionTableExists()) {
            return;
        }

        $submissions = $this->wpdb->get_results(
            $this->wpdb->prepare("SELECT * FROM %i", $this->submission_table),
            ARRAY_A
        );
        if (!is_array($submissions) || empty($submissions)) {
            update_option('griffinforms_submission_history_backfilled', current_time('mysql', true));
            return;
        }

        foreach ($submissions as $submission) {
            $submission_id = isset($submission['id']) ? (int) $submission['id'] : 0;
            $form_id = isset($submission['form_id']) ? (int) $submission['form_id'] : 0;
            if ($submission_id <= 0 || $form_id <= 0) {
                continue;
            }

            $existing = $this->wpdb->get_var(
                $this->wpdb->prepare(
                    "SELECT id FROM %i WHERE submission_id = %d LIMIT 1",
                    $this->revision_table,
                    $submission_id
                )
            );
            if (!empty($existing)) {
                continue;
            }

            $snapshot = $this->decodeSnapshot($submission['submission'] ?? []);
            if (empty($snapshot) || !is_array($snapshot)) {
                $snapshot = array();
            }

            $revision = $this->recordRevision(
                $submission_id,
                $form_id,
                $snapshot,
                'create',
                array(
                    'actor_user_id' => isset($submission['author']) ? (int) $submission['author'] : 0,
                    'actor_label' => isset($submission['author']) ? (string) $submission['author'] : '',
                    'previous_snapshot' => array(),
                    'update_pointer' => false,
                )
            );

            if (!empty($revision['id'])) {
                $this->setCurrentRevisionId($submission_id, (int) $revision['id']);
            }
        }

        update_option('griffinforms_submission_history_backfilled', current_time('mysql', true));
    }

    protected function getNextRevisionNumber(int $submission_id): int
    {
        $current = $this->wpdb->get_var(
            $this->wpdb->prepare(
                "SELECT COALESCE(MAX(revision_number), 0) FROM %i WHERE submission_id = %d",
                $this->revision_table,
                $submission_id
            )
        );

        return ((int) $current) + 1;
    }

    protected function getRevisionById(int $revision_id): ?array
    {
        if ($revision_id <= 0) {
            return null;
        }

        $row = $this->wpdb->get_row(
            $this->wpdb->prepare(
                "SELECT * FROM %i WHERE id = %d LIMIT 1",
                $this->revision_table,
                $revision_id
            ),
            ARRAY_A
        );

        if (!is_array($row)) {
            return null;
        }

        return $this->normalizeRevisionRow($row);
    }

    protected function normalizeRevisionRow(array $row): array
    {
        $row['id'] = isset($row['id']) ? (int) $row['id'] : 0;
        $row['submission_id'] = isset($row['submission_id']) ? (int) $row['submission_id'] : 0;
        $row['form_id'] = isset($row['form_id']) ? (int) $row['form_id'] : 0;
        $row['revision_number'] = isset($row['revision_number']) ? (int) $row['revision_number'] : 0;
        $row['actor_user_id'] = isset($row['actor_user_id']) ? (int) $row['actor_user_id'] : 0;
        $row['restored_from_revision_id'] = isset($row['restored_from_revision_id']) ? (int) $row['restored_from_revision_id'] : 0;
        $row['restored_from_revision_number'] = 0;
        if ($row['restored_from_revision_id'] > 0) {
            $source_number = (int) $this->wpdb->get_var(
                $this->wpdb->prepare(
                    "SELECT revision_number FROM %i WHERE id = %d LIMIT 1",
                    $this->revision_table,
                    $row['restored_from_revision_id']
                )
            );
            $row['restored_from_revision_number'] = $source_number;
        }
        $row['snapshot'] = $this->decodeSnapshot($row['snapshot_json'] ?? array());
        $row['delta'] = $this->decodeSnapshot($row['delta_json'] ?? array());
        $row['summary'] = $this->summarizeRevision($row);
        return $row;
    }

    protected function decodeSnapshot($value): array
    {
        if (is_array($value)) {
            return $value;
        }

        if (!is_string($value) || $value === '') {
            return array();
        }

        $decoded = maybe_unserialize($value);
        if (is_array($decoded)) {
            return $decoded;
        }

        $json = json_decode($value, true);
        if (json_last_error() === JSON_ERROR_NONE && is_array($json)) {
            return $json;
        }

        return array();
    }

    protected function buildDelta(array $previous, array $current): array
    {
        $delta = array();
        $keys = array_unique(array_merge(array_keys($previous), array_keys($current)));

        foreach ($keys as $key) {
            $before = array_key_exists($key, $previous) ? $previous[$key] : null;
            $after = array_key_exists($key, $current) ? $current[$key] : null;
            if ($this->normalizeComparableValue($before) === $this->normalizeComparableValue($after)) {
                continue;
            }

            $delta[$key] = array(
                'before' => $this->normalizeSnapshotValue($before),
                'after' => $this->normalizeSnapshotValue($after),
            );
        }

        return $delta;
    }

    protected function normalizeComparableValue($value): string
    {
        if (is_array($value)) {
            return wp_json_encode($value);
        }

        if ($value === null) {
            return '';
        }

        return (string) $value;
    }

    protected function normalizeSnapshotValue($value)
    {
        if (is_array($value)) {
            $normalized = array();
            foreach ($value as $key => $item) {
                $normalized[$key] = $this->normalizeSnapshotValue($item);
            }
            return $normalized;
        }

        if (is_object($value)) {
            return $this->normalizeSnapshotValue((array) $value);
        }

        if ($value === null) {
            return '';
        }

        return is_bool($value) ? (int) $value : $value;
    }

    protected function summarizeRevision(array $row): string
    {
        $event_type = isset($row['event_type']) ? (string) $row['event_type'] : '';
        $revision_number = isset($row['revision_number']) ? (int) $row['revision_number'] : 0;
        $delta = isset($row['delta']) && is_array($row['delta']) ? $row['delta'] : array();

        if ($event_type === 'create') {
            return __('Initial submission snapshot.', 'griffinforms-form-builder');
        }

        if ($event_type === 'restore') {
            $source_number = isset($row['restored_from_revision_number']) ? (int) $row['restored_from_revision_number'] : 0;
            return $source_number > 0
                ? sprintf(__('Restored from revision #%d.', 'griffinforms-form-builder'), $source_number)
                : __('Restored from an earlier snapshot.', 'griffinforms-form-builder');
        }

        if (empty($delta)) {
            return __('Submission updated.', 'griffinforms-form-builder');
        }

        $count = count($delta);
        if ($count === 1) {
            $key = array_key_first($delta);
            return sprintf(__('Updated %s.', 'griffinforms-form-builder'), $this->formatDeltaKey((string) $key));
        }

        return sprintf(__('Updated %d submission values.', 'griffinforms-form-builder'), $count);
    }

    protected function formatDeltaKey(string $key): string
    {
        $key = str_replace(array('field_', '_'), array('field ', ' '), $key);
        return trim($key);
    }

    protected function setCurrentRevisionId(int $submission_id, int $revision_id): void
    {
        if ($submission_id <= 0 || $revision_id <= 0) {
            return;
        }

        $this->wpdb->update(
            $this->submission_table,
            array('current_revision_id' => $revision_id),
            array('id' => $submission_id),
            array('%d'),
            array('%d')
        );
    }

    protected function revisionTableExists(): bool
    {
        return $this->columnExists($this->revision_table, 'id');
    }

    protected function columnExists(string $table, string $column): bool
    {
        if ($table === '' || $column === '') {
            return false;
        }

        $found = $this->wpdb->get_var(
            $this->wpdb->prepare(
                "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = %s AND COLUMN_NAME = %s",
                $table,
                $column
            )
        );

        return !empty($found);
    }

    protected function isComplianceEnabled(int $form_id): bool
    {
        if ($form_id <= 0) {
            return false;
        }

        $row = $this->wpdb->get_row(
            $this->wpdb->prepare(
                "SELECT compliance_mode FROM %i WHERE id = %d LIMIT 1",
                $this->config->getTable('form'),
                $form_id
            ),
            ARRAY_A
        );

        return is_array($row) && !empty($row['compliance_mode']);
    }

    public function deleteRevisionsBySubmissionId(int $submission_id): int
    {
        if ($submission_id <= 0 || !$this->revisionTableExists()) {
            return 0;
        }

        $deleted = $this->wpdb->delete(
            $this->revision_table,
            ['submission_id' => $submission_id],
            ['%d']
        );

        return $deleted !== false ? (int) $deleted : 0;
    }

    public function deleteOrphanedRevisions(): int
    {
        if (!$this->revisionTableExists()) {
            return 0;
        }

        $revision_table  = esc_sql($this->revision_table);
        $submission_table = esc_sql($this->submission_table);

        $this->wpdb->query(
            "DELETE r FROM `{$revision_table}` r
             LEFT JOIN `{$submission_table}` s ON s.id = r.submission_id
             WHERE s.id IS NULL"
        );

        return (int) $this->wpdb->rows_affected;
    }

    protected function logRestoreEvent(int $submission_id, int $revision_number, bool $success): void
    {
        if (!$this->log) {
            return;
        }

        $message = $success
            ? sprintf(__('Submission restored to revision #%d.', 'griffinforms-form-builder'), $revision_number)
            : __('Submission rollback failed.', 'griffinforms-form-builder');
        $this->log->add(
            $success ? 'warning' : 'error',
            'submission',
            $submission_id,
            get_current_user_id(),
            $message,
            'submission_edit_history'
        );
    }
}
