<?php

namespace RTMCustomMails\Email;

use ReflectionClass;
use ReflectionException;
use RTMCustomMails\EmailOptions\AbstractEmailOption;
use RTMCustomMails\Renderer\TimberEmailRenderer;
use RTMCustomMails\Sender\WPEmailSender;
use RTMCustomMails\SerializerFactory;
use RTMMailComposer\Model\CustomMail;
use RTMMailComposer\Model\EmailSection;
use Symfony\Component\Serializer\Serializer;
use Timber\Timber;
use WC_Email;

/**
 * @template T
 * @class RTMCustomEmail<T>
 *
 * @property T $object
 */
abstract class RTMCustomEmail extends WC_Email implements CustomMail
{

    protected ?string $recipient_type;

    protected array $renderContext = [];

    protected bool $lastResult = false;

    protected string $object_type = 'order';

    protected array $triggerHooks = [];
    protected Serializer $serializer;

    protected array $options = [];

    public function __construct(protected WPEmailSender $emailSender = new WPEmailSender(), ?Serializer $serializer = null)
    {
        $this->serializer = $serializer ?? SerializerFactory::create();
        parent::__construct();

        $this->initTriggerHooks();

        add_filter("wrce_email_render_context_$this->id", [$this, 'prepareContext']);
    }

    /**
     * @template T of AbstractEmailOption
     * @param class-string<T> $optionClass
     * @return void
     */
    public function addOption(string $optionClass)
    {
        $this->options[] = new $optionClass($this);
    }

    public static function asCustomEmail(WC_Email $email): RTMCustomEmail
    {
        if ($email instanceof RTMCustomEmail) {
            return $email;
        }

        return new class($email) extends RTMCustomEmail {
            private WC_Email $proxy;

            public function __construct(WC_Email $proxy)
            {
                $this->proxy = $proxy;

                foreach (get_object_vars($proxy) as $key => $value) {
                    $this->{$key} = $value;
                }
                parent::__construct();
            }

            public function prepareContext($baseContext = []): array
            {
                if (property_exists($this->proxy, 'find') && property_exists($this->proxy, 'replace')) {
                    foreach ($this->proxy->find as $key => $template) {
                        $this->placeholders[$template] = $this->proxy->replace[$key];
                    }
                }

                return parent::prepareContext($baseContext);
            }

            public function trigger(...$args)
            {
                $this->proxy->trigger(...$args);
            }

            public function getTriggerHooks(): array
            {
                return [];
            }
        };
    }

    /**
     * @return mixed
     */
    abstract public function trigger();

    /**
     * @return string[]|array[]
     */
    abstract public function getTriggerHooks(): array;

    public function send($to, $subject, $message, $headers, $attachments)
    {
        do_action('wrce_before_send', $this, $to, $subject, $message, $headers, $attachments);
        return $this->lastResult = $this->emailSender->send($to, $subject, $message, $headers, $attachments, $this);
    }

    /**
     * @return bool
     */
    public function getLastResult(): bool
    {
        return $this->lastResult;
    }

    /**
     * @return bool|string
     */
    public function get_content_html()
    {
        global $is_rendering_email;
        $is_rendering_email = true;

        // temporarily reset context
        $currentContext = Timber::$context_cache;
        Timber::$context_cache = [];

        //prepare context to be used in twig rendering

        $renderedEmail = TimberEmailRenderer::instance()->render($this, $this->object, $this->prepareContext());

        Timber::$context_cache = $currentContext;

        $is_rendering_email = false;

        return $renderedEmail;
    }

    /**
     * Extension for recipient types;
     * @return string
     */
    public function getRecipientType(): string
    {
        return strtolower($this->recipient_type ?? ($this->customer_email ? 'customer' : 'admin'));
    }

    /**
     * Initialize hook on which this email is triggered.
     *
     * This method applies filter `wrce_email_trigger_hooks` with arguments of $this->getTriggerHooks() and the email
     * instance.
     * For each defined hook, if it is a valid string or array with function definitions, call `add_action`.
     *
     * @return void
     * @throws ReflectionException
     */
    public function initTriggerHooks(): void
    {
        // trigger hooks are filterable and expandable with hook 'wrce_trigger_hooks_{email_id}'
        $hooks = apply_filters('wrce_email_trigger_hooks', $this->getTriggerHooks(), $this);

        foreach ($hooks as $hook) {
            // set default values
            $triggerFunction = 'trigger';
            $priority = 10;

            // when a hook is an array, the array may contain the following keys, or be in order
            // with default values if not set:
            // - 1 or 'function'    => 'trigger'
            // - 2 or 'priority'    => 10
            // - 3 or 'args'        => false
            if (is_array($hook)) {
                if ($f = ($hook[1] ?? $hook['function'] ?? false)) {
                    $triggerFunction = $f;
                }

                if ($p = ($hook[2] ?? $hook['priority'] ?? false)) {
                    $priority = $p;
                }

                if ($a = ($hook[3] ?? $hook['args'] ?? false)) {
                    $args = $a;
                }

                $hook = $hook[0];
            } elseif (!is_string($hook)) {
                // anything other than string or array is skipped
                continue;
            }

            // use reflection to get parameter amount if not set
            $args ??= (new ReflectionClass($this))
                ->getMethod($triggerFunction)
                ->getNumberOfParameters();

            // add the trigger action
            add_action($hook, [$this, $triggerFunction], $priority, $args);

            $this->triggerHooks[] = $hook;
        }
    }

    /**
     * Set the render context
     * @param array $context
     * @return void
     */
    public function setRenderContext(array $context): void
    {
        $this->renderContext = $context;
    }

    /**
     * Get the set context and Timber context;
     * @return array
     */
    public function getRenderContext(): array
    {
        return $this->renderContext;
    }

    /**
     * @return string
     */
    public function getObjectType(): string
    {
        return $this->object_type;
    }


    /**
     * @return EmailSection[]
     */
    public function getSections(): array
    {
        $sections = maybe_unserialize($this->get_option('sections', []));
        if (!is_array($sections)) {
            return [];
        }

        // ensure correct array indexing
        $sections = array_values($sections);

        return array_map(
            fn(array $section) => $this->serializer->denormalize($section, EmailSection::class, 'array'),
            $sections);
    }

    public function setSections(array $sections): bool
    {
        $sections = array_map(
            fn(EmailSection $section) => $this->serializer->normalize($section),
            $sections
        );

        return $this->update_option('sections', $sections);
    }

    public function is_manual()
    {
        return (bool)$this->triggerHooks;
    }

    public function getAttachedDocuments()
    {
        return apply_filters('wrce_attached_documents', [], $this);
    }

    /**
     * Replaces the {@see \WC_Settings_API::get_form_fields()} function by iterating over the form fields instead of
     * array_map'ing, to support generator functions for form fields.
     *
     * @return array of options
     */
    #[\Override]
    public function get_form_fields() {
        foreach ($this->form_fields as $key => $value) {
            $this->form_fields[$key] = $this->set_defaults($value);
        }
        return apply_filters( 'woocommerce_settings_api_form_fields_' . $this->id, $this->form_fields);
    }

    protected function prepareContext(array $baseContext = []): array
    {
        foreach ($this->find ?? [] as $key => $template) {
            $baseContext[substr($template, 1, strlen($template) - 2)] = $this->replace[$key];
        }
        foreach ($this->placeholders ?? [] as $template => $value) {
            $baseContext[substr($template, 1, strlen($template) - 2)] = $value;
        }
        return $baseContext;
    }
}
