<?php
/**
 * Reads and writes withdrawal request records.
 *
 * @package SkyServers\Retragere
 */

declare(strict_types=1);

namespace SkyServers\Retragere\Wp;

use SkyServers\Retragere\Core\DeadlineCalculator;
use SkyServers\Retragere\Core\ReferenceToken;
use SkyServers\Retragere\Core\RequestLine;
use SkyServers\Retragere\Core\WithdrawalRequest;
use WP_Post;

/**
 * post_date_gmt is the legal timestamp and post_content is the declaration as
 * submitted. Neither is ever rewritten.
 */
final class Repository
{
    public const META_TOKEN       = '_ss_token_hash';
    public const META_ORDER       = '_ss_order_id';
    public const META_USER        = '_ss_customer_user';
    public const META_NAME        = '_ss_name';
    public const META_EMAIL       = '_ss_email';
    public const META_REF         = '_ss_contract_ref';
    public const META_REASON      = '_ss_reason';
    public const META_LINES       = '_ss_lines';
    public const META_TYPE        = '_ss_type';
    public const META_IP          = '_ss_ip';
    public const META_UA          = '_ss_user_agent';
    public const META_SNAPSHOT    = '_ss_snapshot';
    public const META_TRANSPORT   = '_ss_transport';
    public const META_MAIL_CLIENT = '_ss_email_client_gmt';
    public const META_MAIL_ADMIN  = '_ss_email_admin_gmt';
    public const META_ATTEMPTS    = '_ss_email_attempts';
    public const META_B2B         = '_ss_b2b';
    public const META_SETTLED     = '_ss_settled_gmt';
    public const META_DECISION_GMT     = '_ss_decision_gmt';
    public const META_DECISION_BY      = '_ss_decision_by';
    public const META_DECISION_MESSAGE = '_ss_decision_message';

    /**
     * @param array<string,mixed> $context
     */
    public static function save(WithdrawalRequest $request, array $context): int
    {
        $submittedGmt = (string) $context['submitted_gmt'];

        $id = wp_insert_post(
            [
                'post_type'     => PostType::NAME,
                'post_status'   => PostType::STATUS_NOUA,
                'post_title'    => 'RC',
                'post_content'  => $request->declarationText(),
                'post_date_gmt' => $submittedGmt,
                'post_date'     => get_date_from_gmt($submittedGmt),
            ],
            true
        );

        if (is_wp_error($id) || 0 === $id) {
            return 0;
        }

        $id = (int) $id;

        $lines = [];
        foreach ($request->lines() as $line) {
            $lines[] = $line->toArray();
        }

        update_post_meta($id, self::META_TOKEN, (string) $context['token_hash']);
        update_post_meta($id, self::META_ORDER, (int) $request->orderId());
        update_post_meta($id, self::META_USER, (int) $context['customer_user']);
        update_post_meta($id, self::META_NAME, $request->name());
        update_post_meta($id, self::META_EMAIL, $request->email());
        update_post_meta($id, self::META_REF, $request->contractRef());
        update_post_meta($id, self::META_REASON, $request->reason());
        update_post_meta($id, self::META_LINES, $lines);
        update_post_meta($id, self::META_TYPE, $request->type());
        update_post_meta($id, self::META_IP, (string) $context['ip']);
        update_post_meta($id, self::META_UA, (string) $context['user_agent']);
        update_post_meta($id, self::META_SNAPSHOT, $context['snapshot']);
        update_post_meta($id, self::META_ATTEMPTS, 0);
        update_post_meta($id, self::META_B2B, (string) ($context['b2b_signal'] ?? ''));

        wp_update_post(
            [
                'ID'         => $id,
                'post_title' => self::reference($id),
            ]
        );

        return $id;
    }

    public static function reference(int $id): string
    {
        $post = get_post($id);
        $year = $post instanceof WP_Post
            ? (int) gmdate('Y', (int) strtotime((string) $post->post_date_gmt))
            : (int) gmdate('Y');

        return ReferenceToken::reference($year, $id);
    }

    /**
     * @return array<string,mixed>
     */
    public static function snapshot(int $id): array
    {
        return self::decodeStored(get_post_meta($id, self::META_SNAPSHOT, true));
    }

    public static function request(int $id): ?WithdrawalRequest
    {
        $post = get_post($id);

        if (!$post instanceof WP_Post || PostType::NAME !== $post->post_type) {
            return null;
        }

        $rawLines = self::decodeStored(get_post_meta($id, self::META_LINES, true));
        $lines    = [];

        if (is_array($rawLines)) {
            foreach ($rawLines as $line) {
                if (is_array($line)) {
                    $lines[] = RequestLine::fromArray($line);
                }
            }
        }

        $orderId = (int) get_post_meta($id, self::META_ORDER, true);
        $total   = 0;
        foreach ($lines as $line) {
            $total += $line->qty();
        }

        return WithdrawalRequest::create(
            [
                'name'         => (string) get_post_meta($id, self::META_NAME, true),
                'email'        => (string) get_post_meta($id, self::META_EMAIL, true),
                'contract_ref' => (string) get_post_meta($id, self::META_REF, true),
                'reason'       => (string) get_post_meta($id, self::META_REASON, true),
            ],
            $lines,
            $orderId,
            $total
        );
    }

    /**
     * Quantities already covered by active requests, keyed by order item id.
     *
     * Rejected requests are excluded on purpose: refusing a request must give
     * the quantity back, otherwise a customer could be locked out of a return
     * by a decision that went against them.
     *
     * @return array<int,int>
     */
    public static function requestedQuantities(int $orderId): array
    {
        if ($orderId <= 0) {
            return [];
        }

        $query = new \WP_Query(
            [
                'post_type'      => PostType::NAME,
                'post_status'    => [
                    PostType::STATUS_NOUA,
                    PostType::STATUS_APROBATA,
                    PostType::STATUS_RAMBURSATA,
                ],
                'posts_per_page' => 200,
                'fields'         => 'ids',
                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- bounded lookup on a small post type.
                'meta_query'     => [
                    [
                        'key'   => self::META_ORDER,
                        'value' => $orderId,
                    ],
                ],
            ]
        );

        $totals = [];

        foreach ($query->posts as $id) {
            $lines = self::decodeStored(get_post_meta((int) $id, self::META_LINES, true));

            if (!is_array($lines)) {
                continue;
            }

            foreach ($lines as $line) {
                if (!is_array($line)) {
                    continue;
                }

                $itemId          = (int) ($line['item_id'] ?? 0);
                $totals[$itemId] = ($totals[$itemId] ?? 0) + (int) ($line['qty'] ?? 0);
            }
        }

        return $totals;
    }

    /**
     * Reads a stored structure, whatever shape it was written in.
     *
     * Arrays are what we write now: WordPress serialises them and leaves the
     * characters alone. Records written earlier hold JSON, which update_post_meta
     * damaged on the way in - it passes values through wp_unslash(), stripping
     * the backslash out of every unicode escape. Those come back with broken
     * escapes, which is the best that can be done for them.
     *
     * @param mixed $stored
     *
     * @return array<mixed>
     */
    private static function decodeStored($stored): array
    {
        if (is_array($stored)) {
            return $stored;
        }

        $decoded = json_decode((string) $stored, true);

        return is_array($decoded) ? $decoded : [];
    }

public static function settledAt(int $id): string
    {
        return (string) get_post_meta($id, self::META_SETTLED, true);
    }

    /**
     * Derived rather than stored: the deadline is a fixed number of days after
     * the request arrived, and post_date_gmt already holds that moment. A stored
     * copy could only ever disagree with it.
     */
    public static function refundDueAt(int $id): string
    {
        $post = get_post($id);

        if (!$post instanceof WP_Post) {
            return '';
        }

        $submitted = strtotime((string) $post->post_date_gmt . ' UTC');

        if (false === $submitted) {
            return '';
        }

        return gmdate('Y-m-d H:i:s', $submitted + (DeadlineCalculator::REFUND_DAYS * DAY_IN_SECONDS));
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    public static function lines(int $id): array
    {
        $lines = self::decodeStored(get_post_meta($id, self::META_LINES, true));

        return is_array($lines) ? $lines : [];
    }

    public static function markEmailSent(int $id, string $which, string $gmt): void
    {
        $key = 'client' === $which ? self::META_MAIL_CLIENT : self::META_MAIL_ADMIN;
        update_post_meta($id, $key, $gmt);
    }

    public static function attempts(int $id): int
    {
        return (int) get_post_meta($id, self::META_ATTEMPTS, true);
    }

    public static function bumpAttempts(int $id): int
    {
        $next = self::attempts($id) + 1;
        update_post_meta($id, self::META_ATTEMPTS, $next);

        return $next;
    }

    public static function setTransport(int $id, string $transport): void
    {
        update_post_meta($id, self::META_TRANSPORT, $transport);
    }
}
