<?php
declare(strict_types=1);

namespace Celena\Plugin\Shop\Controllers;

use Celena\Core\Database\Connection;
use Celena\Core\Http\Request;
use Celena\Core\Http\Response;
use Celena\Core\Plugin\Hook;
use Celena\Core\Security\Auth;
use Celena\Core\Security\Csrf;
use Celena\Core\Security\Filter;
use Celena\Core\Template\Engine;
use Celena\Module\Settings\Options;
use Celena\Plugin\Shop\Support\CustomerRepo;

/**
 * Корзина на клиенте в localStorage. В checkout POST приходит JSON-массив
 * {product_id, quantity}; здесь мы проверяем товары по БД, игнорируя любые
 * цены/названия, присланные клиентом — это источник истины.
 */
final class CheckoutController
{
    public function __construct(
        private readonly Engine $engine,
        private readonly Connection $conn,
        private readonly Auth $auth,
        private readonly Options $options,
    ) {
    }

    public function show(Request $request): Response
    {
        $user = $this->auth->user() ?? [];
        $this->engine->setTheme((string) $this->options->get('theme', 'default'));

        $shop = new \Celena\Plugin\Shop\Support\ShopSettings($this->options);

        // Активные способы доставки/оплаты — только для отображения на чекауте.
        // Цены форматируем здесь, чтобы шаблоны не звали хелперы.
        $shippingRows = $this->conn->builder('shop_shipping_methods')
            ->where('is_active', '=', true)
            ->orderBy('sort_order', 'asc')->orderBy('id', 'asc')->get();
        $shipping = [];
        foreach ($shippingRows as $r) {
            $cost = (float) ($r['cost'] ?? 0);
            $shipping[] = [
                'id'          => (int) $r['id'],
                'slug'        => (string) $r['slug'],
                'title'       => (string) $r['title'],
                'description' => (string) ($r['description'] ?? ''),
                'mode'        => (string) ($r['mode'] ?? 'flat'),
                'cost'        => $cost,
                'cost_fmt'    => $cost > 0 ? $shop->formatPrice($cost) : '',
                'is_free'     => $cost <= 0 || ($r['mode'] ?? '') === 'free' || ($r['mode'] ?? '') === 'pickup',
            ];
        }

        $paymentRows = $this->conn->builder('shop_payment_methods')
            ->where('is_active', '=', true)
            ->orderBy('sort_order', 'asc')->orderBy('id', 'asc')->get();
        $payments = [];
        foreach ($paymentRows as $r) {
            $payments[] = [
                'id'          => (int) $r['id'],
                'slug'        => (string) $r['slug'],
                'title'       => (string) $r['title'],
                'description' => (string) ($r['description'] ?? ''),
                'kind'        => (string) ($r['kind'] ?? 'cash'),
            ];
        }

        $html = $this->engine->render('plugins/shop/front/checkout', [
            'site' => ['name' => (string) $this->options->get('site_name', '')],
            'page' => ['title' => 'Оформление', 'seo_title' => 'Оформление', 'seo_description' => '', 'og_image' => ''],
            'user' => ['name' => (string) ($user['name'] ?? ''), 'email' => (string) ($user['email'] ?? '')],
            'error' => (string) $request->query('err', ''),
            'shipping_methods' => $shipping,
            'payment_methods'  => $payments,
            'has_shipping'     => $shipping !== [] ? '1' : '',
            'has_payments'     => $payments !== [] ? '1' : '',
            'currency_symbol'  => $shop->currencySymbol(),
        ]);
        return Response::html($html);
    }

    public function submit(Request $request): Response
    {
        if (!Csrf::validate((string) $request->input('_csrf'))) {
            return Response::redirect('/checkout?err=csrf');
        }

        $cartRaw = (string) $request->input('cart', '[]');
        $requested = json_decode($cartRaw, true);
        if (!is_array($requested) || $requested === []) {
            return Response::redirect('/checkout?err=empty');
        }

        // Корзина с клиента: каждая строка — пара (product_id, variation_id).
        // Один товар может встречаться несколько раз с разными variation_id.
        $lines = [];   // ключ "pid:vid" → ['pid','vid','qty','label']
        $ids = [];
        foreach ($requested as $line) {
            if (!is_array($line)) continue;
            $pid = Filter::int($line['product_id'] ?? 0, 0);
            $vid = Filter::int($line['variation_id'] ?? 0, 0);
            $qty = Filter::int($line['quantity'] ?? 0, 0);
            if ($pid <= 0 || $qty <= 0) continue;
            $key = $pid . ':' . $vid;
            if (isset($lines[$key])) {
                $lines[$key]['qty'] += $qty;
            } else {
                $lines[$key] = [
                    'pid'   => $pid,
                    'vid'   => $vid,
                    'qty'   => $qty,
                    'label' => (string) ($line['variation_label'] ?? ''),
                ];
            }
            $ids[] = $pid;
        }
        if ($lines === []) {
            return Response::redirect('/checkout?err=empty');
        }

        $products = $this->conn->builder('shop_products')
            ->whereIn('id', array_values(array_unique($ids)))
            ->where('is_active', '=', true)
            ->get();
        $byId = [];
        foreach ($products as $p) $byId[(int) $p['id']] = $p;

        $items = [];
        $total = 0.0;
        foreach ($lines as $line) {
            $p = $byId[$line['pid']] ?? null;
            if ($p === null) continue;

            // Если у товара есть вариации — обязаны выбрать, и цена берётся из вариации
            $variations = [];
            if (!empty($p['variations'])) {
                $decoded = json_decode((string) $p['variations'], true);
                if (is_array($decoded)) $variations = $decoded;
            }

            $price = (float) $p['price'];
            $sku   = (string) ($p['sku'] ?? '');
            $variationLabel = '';
            $variationId = 0;

            if ($variations !== []) {
                if ($line['vid'] <= 0) continue; // у товара с вариациями vid обязателен
                $matched = null;
                foreach ($variations as $v) {
                    if ((int) ($v['id'] ?? 0) === $line['vid']) { $matched = $v; break; }
                }
                if ($matched === null) continue;
                $price = (float) ($matched['price'] ?? $price);
                $sku   = (string) ($matched['sku'] ?? $sku);
                $variationId = (int) $matched['id'];
                // Восстанавливаем label из самой вариации, не доверяя клиенту
                $valuesArr = is_array($matched['values'] ?? null) ? $matched['values'] : [];
                $opts = [];
                if (!empty($p['options'])) {
                    $decodedOpts = json_decode((string) $p['options'], true);
                    if (is_array($decodedOpts)) $opts = $decodedOpts;
                }
                $parts = [];
                foreach ($valuesArr as $idx => $val) {
                    $optName = (string) ($opts[$idx]['name'] ?? '');
                    $parts[] = ($optName !== '' ? $optName . ': ' : '') . $val;
                }
                $variationLabel = implode(' · ', $parts);
            }

            $sub = $price * $line['qty'];
            $items[] = [
                'product_id'      => $line['pid'],
                'variation_id'    => $variationId,
                'variation_label' => $variationLabel,
                'title'           => (string) $p['title'],
                'sku'             => $sku,
                'price'           => $price,
                'quantity'        => $line['qty'],
                'subtotal'        => $sub,
            ];
            $total += $sub;
        }
        if ($items === []) {
            return Response::redirect('/checkout?err=items');
        }

        $name  = Filter::string($request->input('name'), 191);
        $email = Filter::email($request->input('email'));
        $phone = Filter::string($request->input('phone'), 32);
        $addr  = Filter::string($request->input('address'), 500);
        $comment = Filter::string($request->input('comment'), 1000);
        if ($name === '' || $email === null) {
            return Response::redirect('/checkout?err=invalid');
        }

        // Резолв способа доставки. Если ничего не передали — оставляем без метода.
        $shippingId = Filter::int($request->input('shipping_method_id'), 0);
        $shippingMethodId = null;
        $shippingMethodTitle = null;
        $shippingCost = 0.0;
        if ($shippingId > 0) {
            $sm = $this->conn->builder('shop_shipping_methods')
                ->where('id', '=', $shippingId)
                ->where('is_active', '=', true)
                ->first();
            if ($sm !== null) {
                $shippingMethodId = (int) $sm['id'];
                $shippingMethodTitle = (string) $sm['title'];
                $shippingCost = (float) ($sm['cost'] ?? 0);
                $freeFrom = $sm['free_from'] ?? null;
                if ($freeFrom !== null && $freeFrom !== '' && $total >= (float) $freeFrom) {
                    $shippingCost = 0.0;
                }
                if (($sm['mode'] ?? '') === 'pickup' || ($sm['mode'] ?? '') === 'free') {
                    $shippingCost = 0.0;
                }
            }
        }

        // Резолв способа оплаты.
        $paymentId = Filter::int($request->input('payment_method_id'), 0);
        $paymentMethodId = null;
        $paymentMethodTitle = null;
        if ($paymentId > 0) {
            $pm = $this->conn->builder('shop_payment_methods')
                ->where('id', '=', $paymentId)
                ->where('is_active', '=', true)
                ->first();
            if ($pm !== null) {
                $paymentMethodId = (int) $pm['id'];
                $paymentMethodTitle = (string) $pm['title'];
            }
        }

        // Промокод. Применяем при валидности; иначе молча игнорируем (без ошибки).
        $promoCodeRaw = strtoupper(trim((string) $request->input('promo_code', '')));
        $promoCode = preg_match('/^[A-Z0-9_\-]{3,64}$/', $promoCodeRaw) === 1 ? $promoCodeRaw : '';
        $discount = 0.0;
        $appliedPromo = '';
        $promoRow = null;
        if ($promoCode !== '') {
            $promoRow = $this->conn->builder('shop_promo_codes')
                ->where('code', '=', $promoCode)
                ->where('is_active', '=', true)
                ->first();
            if ($promoRow !== null && $this->isPromoUsable($promoRow, $total)) {
                $type = (string) ($promoRow['type'] ?? 'percent');
                $value = (float) ($promoRow['value'] ?? 0);
                if ($type === 'percent') {
                    $discount = max(0.0, min($total, round($total * $value / 100, 2)));
                } elseif ($type === 'fixed') {
                    $discount = max(0.0, min($total, $value));
                } elseif ($type === 'free_shipping') {
                    $shippingCost = 0.0;
                }
                $appliedPromo = $promoCode;
            }
        }

        $grandTotal = max(0.0, round($total - $discount + $shippingCost, 2));

        // Источник истины для валюты — настройки магазина (shop.currency_code).
        // Старый ключ shop_currency оставлен как fallback ради совместимости.
        $shopSettings = new \Celena\Plugin\Shop\Support\ShopSettings($this->options);
        $currency = $shopSettings->currencyCode() ?: (string) $this->options->get('shop_currency', 'USD');
        $prefix = $shopSettings->get('shop.order_number_prefix', 'CS-');
        $number = $prefix . strtoupper(bin2hex(random_bytes(4)));
        $now = date('Y-m-d H:i:s');

        // Заводим/обновляем клиента ДО транзакции с заказом — нужен customer_id.
        $repo = new CustomerRepo($this->conn);
        $customerId = $repo->upsertFromOrder([
            'email'   => $email,
            'name'    => $name,
            'phone'   => $phone,
            'address' => $addr,
            'user_id' => $this->auth->id(),
        ]);

        $orderId = (int) $this->conn->transaction(function () use (
            $number, $name, $email, $phone, $addr, $comment, $total, $currency, $items, $now, $customerId,
            $shippingMethodId, $shippingMethodTitle, $shippingCost,
            $paymentMethodId, $paymentMethodTitle,
            $appliedPromo, $discount, $grandTotal, $promoRow
        ) {
            $uid = $this->auth->id();
            $id = (int) $this->conn->builder('shop_orders')->insert([
                'number'                => $number,
                'user_id'               => $uid,
                'customer_id'           => $customerId ?: null,
                'customer_name'         => $name,
                'customer_email'        => $email,
                'customer_phone'        => $phone,
                'customer_address'      => $addr,
                'comment'               => $comment,
                'subtotal'              => $total,
                'discount'              => $discount,
                'tax'                   => 0,
                'shipping_cost'         => $shippingCost,
                'total'                 => $grandTotal,
                'currency'              => $currency,
                'status'                => 'new',
                'payment_status'        => 'pending',
                'source'                => 'web',
                'shipping_method_id'    => $shippingMethodId,
                'shipping_method_title' => $shippingMethodTitle,
                'payment_method_id'     => $paymentMethodId,
                'payment_method_title'  => $paymentMethodTitle,
                'promo_code'            => $appliedPromo !== '' ? $appliedPromo : null,
                'created_at'            => $now,
                'updated_at'            => $now,
            ]);
            // Инкрементим usage_count промокода (если применили).
            if ($appliedPromo !== '' && $promoRow !== null) {
                try {
                    $this->conn->builder('shop_promo_codes')
                        ->where('id', '=', (int) $promoRow['id'])
                        ->update([
                            'usage_count' => (int) ($promoRow['usage_count'] ?? 0) + 1,
                            'updated_at'  => $now,
                        ]);
                } catch (\Throwable) {}
            }
            foreach ($items as $it) {
                $this->conn->builder('shop_order_items')->insert([
                    'order_id'        => $id,
                    'product_id'      => $it['product_id'],
                    'variation_id'    => $it['variation_id'] ?: null,
                    'variation_label' => $it['variation_label'] ?: null,
                    'product_title'   => $it['title'],
                    'product_sku'     => $it['sku'],
                    'price'           => $it['price'],
                    'quantity'        => $it['quantity'],
                    'subtotal'        => $it['subtotal'],
                ]);
            }
            // Лог истории — заказ создан с витрины.
            try {
                $this->conn->builder('shop_order_history')->insert([
                    'order_id'   => $id,
                    'user_id'    => $uid,
                    'event'      => 'created',
                    'comment'    => 'Оформлен с витрины',
                    'created_at' => $now,
                ]);
            } catch (\Throwable) {}
            return $id;
        });

        // Пересчёт агрегатов клиента после успешного создания.
        if ($customerId > 0) {
            $repo->recalcAggregates($customerId);
        }

        Hook::action('shop.order.created', [
            'id' => $orderId, 'number' => $number, 'total' => $total,
            'email' => $email, 'name' => $name, 'items_count' => count($items),
            'source' => 'web',
        ]);
        return Response::redirect('/checkout/thanks/' . $number);
    }

    /**
     * AJAX-проверка промокода для UI чекаута. Не списывает usage_count —
     * это произойдёт только при успешном POST /checkout. На клиент возвращаем
     * сумму скидки и тип, чтобы виджет мог сразу показать обновлённый total.
     */
    public function promoCheck(Request $request): Response
    {
        if (!Csrf::validate((string) $request->input('_csrf'))) {
            return Response::json(['ok' => false, 'error' => 'csrf', 'message' => 'Сессия истекла. Перезагрузите страницу.'], 419);
        }
        $codeRaw = strtoupper(trim((string) $request->input('code', '')));
        if ($codeRaw === '') {
            return Response::json(['ok' => false, 'error' => 'empty', 'message' => 'Введите промокод.']);
        }
        if (!preg_match('/^[A-Z0-9_\-]{3,64}$/', $codeRaw)) {
            return Response::json(['ok' => false, 'error' => 'format', 'message' => 'Недопустимый формат кода.']);
        }
        $subtotal = max(0.0, (float) $request->input('subtotal', 0));

        $row = $this->conn->builder('shop_promo_codes')
            ->where('code', '=', $codeRaw)
            ->where('is_active', '=', true)
            ->first();
        if ($row === null) {
            return Response::json(['ok' => false, 'error' => 'not_found', 'message' => 'Промокод не найден.']);
        }
        if (!$this->isPromoUsable($row, $subtotal)) {
            $reason = $this->promoFailureReason($row, $subtotal);
            return Response::json(['ok' => false, 'error' => $reason['code'], 'message' => $reason['message']]);
        }

        $type = (string) ($row['type'] ?? 'percent');
        $value = (float) ($row['value'] ?? 0);
        $discount = 0.0;
        if ($type === 'percent') {
            $discount = max(0.0, min($subtotal, round($subtotal * $value / 100, 2)));
        } elseif ($type === 'fixed') {
            $discount = max(0.0, min($subtotal, $value));
        }

        $shop = new \Celena\Plugin\Shop\Support\ShopSettings($this->options);
        $messageParts = [];
        if ($type === 'percent')        $messageParts[] = 'Скидка ' . rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.') . '%';
        elseif ($type === 'fixed')      $messageParts[] = 'Скидка ' . $shop->formatPrice($discount);
        elseif ($type === 'free_shipping') $messageParts[] = 'Бесплатная доставка';

        return Response::json([
            'ok'             => true,
            'code'           => $codeRaw,
            'type'           => $type,
            'value'          => $value,
            'discount'       => $discount,
            'discount_fmt'   => $discount > 0 ? $shop->formatPrice($discount) : '',
            'free_shipping'  => $type === 'free_shipping',
            'message'        => 'Промокод принят. ' . implode(' · ', $messageParts) . '.',
        ]);
    }

    /** Человеко-понятный код+текст ошибки для конкретной невалидности промокода. */
    private function promoFailureReason(array $row, float $subtotal): array
    {
        $now = time();
        if (!empty($row['starts_at']) && strtotime((string) $row['starts_at']) > $now) {
            return ['code' => 'not_started', 'message' => 'Промокод ещё не действует.'];
        }
        if (!empty($row['ends_at']) && strtotime((string) $row['ends_at']) < $now) {
            return ['code' => 'expired', 'message' => 'Срок действия промокода истёк.'];
        }
        $limit = (int) ($row['usage_limit'] ?? 0);
        if ($limit > 0 && (int) ($row['usage_count'] ?? 0) >= $limit) {
            return ['code' => 'limit', 'message' => 'Промокод больше не действует (лимит использований исчерпан).'];
        }
        if (!empty($row['min_amount']) && $subtotal < (float) $row['min_amount']) {
            $shop = new \Celena\Plugin\Shop\Support\ShopSettings($this->options);
            return ['code' => 'min_amount', 'message' => 'Минимальная сумма заказа для этого кода — ' . $shop->formatPrice((float) $row['min_amount']) . '.'];
        }
        return ['code' => 'invalid', 'message' => 'Промокод недействителен.'];
    }

    /**
     * Проверка валидности промокода: даты, лимит использований, минимальная сумма.
     */
    private function isPromoUsable(array $row, float $subtotal): bool
    {
        $now = time();
        $startsAt = $row['starts_at'] ?? null;
        if ($startsAt !== null && $startsAt !== '' && strtotime((string) $startsAt) > $now) {
            return false;
        }
        $endsAt = $row['ends_at'] ?? null;
        if ($endsAt !== null && $endsAt !== '' && strtotime((string) $endsAt) < $now) {
            return false;
        }
        $limit = $row['usage_limit'] ?? null;
        if ($limit !== null && $limit !== '' && (int) $limit > 0
            && (int) ($row['usage_count'] ?? 0) >= (int) $limit) {
            return false;
        }
        $min = $row['min_amount'] ?? null;
        if ($min !== null && $min !== '' && $subtotal < (float) $min) {
            return false;
        }
        return true;
    }

    public function thanks(Request $request, array $params): Response
    {
        $number = strtoupper((string) ($params['number'] ?? ''));
        // Префикс номера настраивается в shop.order_number_prefix — не хардкодим CS-/CEL-/ORD-.
        // Базовая проверка: только латиница/цифры/дефис, разумная длина (8..64).
        if ($number === '' || !preg_match('/^[A-Z0-9\-]{4,64}$/', $number)) {
            return Response::notFound();
        }
        $order = $this->conn->builder('shop_orders')->where('number', '=', $number)->first();
        if ($order === null) {
            return Response::notFound();
        }
        $this->engine->setTheme((string) $this->options->get('theme', 'default'));
        $html = $this->engine->render('plugins/shop/front/thanks', [
            'site' => ['name' => (string) $this->options->get('site_name', '')],
            'page' => ['title' => 'Спасибо за заказ', 'seo_title' => 'Спасибо', 'seo_description' => '', 'og_image' => ''],
            'order' => [
                'number' => (string) $order['number'],
                'total' => number_format((float) $order['total'], 2, '.', ' '),
                'currency' => (string) $order['currency'],
                'email' => (string) $order['customer_email'],
            ],
        ]);
        return Response::html($html);
    }
}
