<?php

namespace EasyLinkAutomations\Automation;

if (!defined('ABSPATH')) {
    exit;
}

use EasyLinkAutomations\Actions\ActionRegistry;
use EasyLinkAutomations\Models\Automation;
use EasyLinkAutomations\Models\AutomationAction;
use EasyLinkAutomations\Models\ExecutionLog;
use EasyLinkAutomations\Triggers\BaseTrigger;

/**
 * Executes automations when triggers fire.
 *
 * Handles the complete automation flow:
 * 1. Find matching automations for a trigger
 * 2. Execute actions in sequence
 * 3. Log execution results
 */
final class AutomationRunner
{
    /**
     * Run all automations for a given trigger.
     *
     * @param string $triggerSlug The trigger that fired (e.g., 'wordpress/post_published')
     * @param array  $triggerData Data from the trigger
     */
    public function run(string $triggerSlug, array $triggerData): void
    {
        // Store a flattened sample of the trigger data so the UI can discover
        // available fields (e.g., nested form fields) via triggers/sample-fields.
        set_transient(
            'easylink_automations_trigger_sample_' . sanitize_key($triggerSlug),
            wp_json_encode(BaseTrigger::flattenFields($triggerData)),
            30 * DAY_IN_SECONDS
        );

        $automations = Automation::findByTrigger($triggerSlug);

        // For incoming webhooks, route to the specific automation by token and HTTP method.
        if ($triggerSlug === 'incoming-webhook') {
            $incomingToken  = $triggerData['token'] ?? '';
            $incomingMethod = strtoupper($triggerData['request_method'] ?? '');

            $automations = array_filter($automations, function (array $automation) use ($incomingToken, $incomingMethod): bool {
                $config = json_decode($automation['trigger_data'] ?? '{}', true) ?? [];

                if (($config['token'] ?? '') !== $incomingToken) {
                    return false;
                }

                $allowedMethods = array_map('trim', explode(',', strtoupper($config['http_method'] ?? 'GET,POST')));

                return in_array($incomingMethod, $allowedMethods, true);
            });
        }

        // Filter by form_id if the trigger config specifies one
        $automations = array_filter($automations, function (array $automation) use ($triggerSlug, $triggerData): bool {
            $config = json_decode($automation['trigger_data'] ?? '{}', true) ?? [];
            if (!empty($config['form_id'])) {
                $match = (string) ($triggerData['form_id'] ?? '') === (string) $config['form_id'];
                if (!$match) {
                    error_log('[EasyLink Automations] form_id mismatch for automation ' . $automation['id']
                        . ' (' . $triggerSlug . '): trigger=' . ($triggerData['form_id'] ?? '') . ' config=' . $config['form_id']);
                }
                return $match;
            }
            return true;
        });
        foreach ($automations as $automation) {
            if ($automation['status'] !== 'active') {
                continue;
            }

            $this->executeAutomation($automation, $triggerData);
        }
    }

    /**
     * Execute a single automation.
     */
    private function executeAutomation(array $automation, array $triggerData): void
    {
        $automationId = $automation['id'];
        $startTime    = microtime(true);
        $results      = [];
        $overallSuccess = true;

        // Check trigger conditions (trigger_data in DB maps to trigger config)
        $triggerConfig = json_decode($automation['trigger_data'] ?? '{}', true) ?? [];
        if (!$this->evaluateConditions($triggerConfig, $triggerData)) {
            $this->log($automationId, 'skipped', $triggerData, [
                'reason' => 'Conditions not met',
            ]);
            return;
        }

        // Get actions for this automation
        $actions = AutomationAction::findByAutomation($automationId);

        // Sort by order
        usort($actions, fn($a, $b) => $a['action_order'] <=> $b['action_order']);

        $currentData   = $triggerData;
        $delayPending  = false;

        foreach ($actions as $index => $actionRow) {
            $actionSlug   = $actionRow['action_slug'];
            $actionConfig = json_decode($actionRow['config'], true) ?? [];

            $action = ActionRegistry::get($actionSlug);
            if (!$action) {
                $results[] = [
                    'action'  => $actionSlug,
                    'success' => false,
                    'message' => __('Action not found', 'easylink-automations'),
                ];
                $overallSuccess = false;
                break;
            }

            // Evaluate per-action conditions — skip if not met
            if (!$this->evaluateActionConditions($actionConfig, $currentData)) {
                $results[] = [
                    'action'  => $actionSlug,
                    'success' => true,
                    'message' => __('Skipped (conditions not met)', 'easylink-automations'),
                    'skipped' => true,
                ];
                continue;
            }

            try {
                // Strip condition metadata before passing config to the action
                $execConfig = $actionConfig;
                unset($execConfig['_conditions']);

                $result = $action->execute($currentData, $execConfig);

                $results[] = [
                    'action'  => $actionSlug,
                    'success' => $result['success'],
                    'message' => $result['message'],
                    'data'    => $result['data'] ?? null,
                ];

                if (!$result['success']) {
                    $overallSuccess = false;
                    break; // Stop on failure
                }

                // Handle delay action
                if (!empty($result['requires_delay'])) {
                    $this->scheduleDelayedContinuation(
                        $automationId,
                        $actions,
                        $index + 1,
                        $currentData,
                        $result['delay_seconds']
                    );
                    $delayPending = true;
                    break;
                }

                // Merge action output data for next action
                if (!empty($result['data'])) {
                    $currentData = array_merge($currentData, $result['data']);
                }

            } catch (\Throwable $e) {
                $results[] = [
                    'action'  => $actionSlug,
                    'success' => false,
                    'message' => $e->getMessage(),
                ];
                $overallSuccess = false;
                break;
            }
        }

        $duration = round((microtime(true) - $startTime) * 1000); // in ms

        $status = $delayPending ? 'running' : ($overallSuccess ? 'success' : 'failed');

        $logId = $this->log(
            $automationId,
            $status,
            $triggerData,
            [
                'duration_ms' => $duration,
                'actions'     => $results,
            ],
            $delayPending
        );

        if ($status === 'failed') {
            $this->maybeSendFailureEmail($automation, $results);
        }

        // Pass the log ID to the delayed execution so it can update this entry.
        if ($delayPending && $logId) {
            $this->updateDelayedPayloadLogId($logId);
        }
    }

    /**
     * Evaluate trigger conditions against data.
     */
    private function evaluateConditions(array $config, array $data): bool
    {
        if (empty($config['conditions'])) {
            return true;
        }

        return $this->evaluateRules($config['conditions'], $data, 'and');
    }

    /**
     * Evaluate action-level conditions stored in the action config.
     *
     * Conditions are stored under the `_conditions` key:
     * { "logic": "and"|"or", "rules": [{ "field": "...", "operator": "...", "value": "..." }] }
     */
    private function evaluateActionConditions(array $actionConfig, array $data): bool
    {
        if (empty($actionConfig['_conditions']['rules'])) {
            return true;
        }

        $conditions = $actionConfig['_conditions'];
        $logic      = $conditions['logic'] ?? 'and';

        // Filter out incomplete rules (empty field name)
        $rules = array_filter(
            $conditions['rules'] ?? [],
            fn(array $rule) => !empty($rule['field'])
        );

        if (empty($rules)) {
            return true;
        }

        return $this->evaluateRules($rules, $data, $logic);
    }

    /**
     * Evaluate a set of condition rules with AND/OR logic.
     */
    private function evaluateRules(array $rules, array $data, string $logic = 'and'): bool
    {
        foreach ($rules as $condition) {
            $field    = $condition['field'] ?? '';
            $operator = $condition['operator'] ?? 'equals';
            $value    = $condition['value'] ?? '';

            $actualValue = $this->resolveField($field, $data);

            $met = match ($operator) {
                'equals'       => $actualValue == $value,
                'not_equals'   => $actualValue != $value,
                'contains'     => is_string($actualValue) && str_contains($actualValue, $value),
                'not_contains' => is_string($actualValue) && !str_contains($actualValue, $value),
                'greater_than' => is_numeric($actualValue) && is_numeric($value) && (float) $actualValue > (float) $value,
                'less_than'    => is_numeric($actualValue) && is_numeric($value) && (float) $actualValue < (float) $value,
                'is_empty'     => empty($actualValue),
                'is_not_empty' => !empty($actualValue),
                'starts_with'  => is_string($actualValue) && str_starts_with($actualValue, $value),
                'ends_with'    => is_string($actualValue) && str_ends_with($actualValue, $value),
                default        => true,
            };

            if ($logic === 'or' && $met) {
                return true;
            }
            if ($logic === 'and' && !$met) {
                return false;
            }
        }

        // AND: all passed; OR: none passed
        return $logic === 'and';
    }

    /**
     * Resolve a field value from data, supporting dot notation for nested access.
     */
    private function resolveField(string $field, array $data): mixed
    {
        // Try direct key first (covers flat and already-flattened data)
        if (array_key_exists($field, $data)) {
            return $data[$field];
        }

        // Dot notation: traverse nested arrays
        if (str_contains($field, '.')) {
            $current = $data;
            foreach (explode('.', $field) as $segment) {
                if (is_array($current) && array_key_exists($segment, $current)) {
                    $current = $current[$segment];
                } else {
                    return null;
                }
            }
            return $current;
        }

        return null;
    }

    /** @var int Timestamp used by the most recent delay scheduling. */
    private int $lastDelayTimestamp = 0;

    /** @var array Payload used by the most recent delay scheduling. */
    private array $lastDelayPayload = [];

    /**
     * Schedule delayed continuation using WP Cron.
     */
    private function scheduleDelayedContinuation(
        int $automationId,
        array $actions,
        int $startIndex,
        array $data,
        int $delaySeconds
    ): void {
        $this->lastDelayPayload = [
            'automation_id' => $automationId,
            'actions'       => array_slice($actions, $startIndex),
            'data'          => $data,
        ];
        $this->lastDelayTimestamp = time() + $delaySeconds;

        $scheduled = wp_schedule_single_event(
            $this->lastDelayTimestamp,
            'easylink_automations_delayed_execution',
            [$this->lastDelayPayload]
        );

        if (!$scheduled) {
            error_log('[EasyLink Automations] Failed to schedule delayed execution for automation ' . $automationId);
        }

        // Clear WP-Cron lock so the next request can trigger cron immediately
        // without waiting for the default 60-second lock timeout.
        delete_transient('doing_cron');
    }

    /**
     * Re-schedule the delayed event with the log ID included in the payload.
     */
    private function updateDelayedPayloadLogId(int $logId): void
    {
        // Unschedule the event we just created (without the log_id)
        wp_unschedule_event(
            $this->lastDelayTimestamp,
            'easylink_automations_delayed_execution',
            [$this->lastDelayPayload]
        );

        // Re-schedule with the log_id included
        $this->lastDelayPayload['log_id'] = $logId;
        wp_schedule_single_event(
            $this->lastDelayTimestamp,
            'easylink_automations_delayed_execution',
            [$this->lastDelayPayload]
        );
    }

    /**
     * Log execution result.
     *
     * @return int|false The log ID or false on failure.
     */
    /**
     * Send a failure notification email if the setting is enabled.
     */
    private function maybeSendFailureEmail(array $automation, array $results): void
    {
        $settings = \EasyLinkAutomations\Config::getOption('settings', []);
        if (empty($settings['email_notifications'])) {
            return;
        }

        $email = !empty($settings['notification_email']) ? $settings['notification_email'] : get_option('admin_email');
        if (!$email) {
            return;
        }

        $title     = $automation['title'] ?? 'Untitled';
        $lastError = '';
        foreach (array_reverse($results) as $r) {
            if (!($r['success'] ?? true)) {
                $lastError = $r['message'] ?? 'Unknown error';
                break;
            }
        }

        $subject = sprintf('[EasyLink Automations] Automation failed: %s', $title);
        $body    = sprintf(
            "The automation \"%s\" (ID: %d) has failed.\n\nError: %s\n\nTime: %s\n\nYou can view the execution logs in your WordPress admin panel.",
            $title,
            $automation['id'],
            $lastError,
            current_time('mysql')
        );

        wp_mail($email, $subject, $body);
    }

    private function log(int $automationId, string $status, array $triggerData, array $result, bool $pending = false): int|false
    {
        $settings = \EasyLinkAutomations\Config::getOption('settings', []);
        if (empty($settings['keep_logs'] ?? true)) {
            return false;
        }

        $now = current_time('mysql');

        return ExecutionLog::create([
            'automation_id' => $automationId,
            'status'        => $status,
            'trigger_data'  => wp_json_encode($triggerData),
            'result_data'   => wp_json_encode($result),
            'error_message' => $status === 'failed' ? ($result['actions'][count($result['actions'] ?? []) - 1]['message'] ?? null) : null,
            'started_at'    => $now,
            'completed_at'  => $pending ? null : $now,
        ]);
    }
}
