<?php
declare(strict_types=1);

namespace App\Modules\Shipments;

use PDO;

final class ShipmentPackageRepository
{
    public function __construct(
        private readonly PDO $pdo
    ) {
    }

    /**
     * @param array<string, mixed> $data
     */
    public function create(array $data): int
    {
        $statement = $this->pdo->prepare(
            'INSERT INTO shipment_packages (
                order_id, provider, delivery_method_id, credentials_id, command_id,
                status, carrier_id, package_type, weight_kg, length_cm, width_cm, height_cm,
                insurance_amount, insurance_currency, cod_amount, cod_currency,
                label_format, receiver_point_id, sender_point_id, reference_number, payload_json
            ) VALUES (
                :order_id, :provider, :delivery_method_id, :credentials_id, :command_id,
                :status, :carrier_id, :package_type, :weight_kg, :length_cm, :width_cm, :height_cm,
                :insurance_amount, :insurance_currency, :cod_amount, :cod_currency,
                :label_format, :receiver_point_id, :sender_point_id, :reference_number, :payload_json
            )'
        );

        $statement->execute([
            'order_id' => (int) ($data['order_id'] ?? 0),
            'provider' => trim((string) ($data['provider'] ?? 'allegro_wza')),
            'delivery_method_id' => $this->nullStr((string) ($data['delivery_method_id'] ?? '')),
            'credentials_id' => $this->nullStr((string) ($data['credentials_id'] ?? '')),
            'command_id' => $this->nullStr((string) ($data['command_id'] ?? '')),
            'status' => trim((string) ($data['status'] ?? 'draft')),
            'carrier_id' => $this->nullStr((string) ($data['carrier_id'] ?? '')),
            'package_type' => trim((string) ($data['package_type'] ?? 'PACKAGE')),
            'weight_kg' => isset($data['weight_kg']) ? (float) $data['weight_kg'] : null,
            'length_cm' => isset($data['length_cm']) ? (float) $data['length_cm'] : null,
            'width_cm' => isset($data['width_cm']) ? (float) $data['width_cm'] : null,
            'height_cm' => isset($data['height_cm']) ? (float) $data['height_cm'] : null,
            'insurance_amount' => isset($data['insurance_amount']) ? (float) $data['insurance_amount'] : null,
            'insurance_currency' => $this->nullStr((string) ($data['insurance_currency'] ?? '')),
            'cod_amount' => isset($data['cod_amount']) ? (float) $data['cod_amount'] : null,
            'cod_currency' => $this->nullStr((string) ($data['cod_currency'] ?? '')),
            'label_format' => trim((string) ($data['label_format'] ?? 'PDF')),
            'receiver_point_id' => $this->nullStr((string) ($data['receiver_point_id'] ?? '')),
            'sender_point_id' => $this->nullStr((string) ($data['sender_point_id'] ?? '')),
            'reference_number' => $this->nullStr((string) ($data['reference_number'] ?? '')),
            'payload_json' => isset($data['payload_json']) ? json_encode($data['payload_json'], JSON_UNESCAPED_UNICODE) : null,
        ]);

        return (int) $this->pdo->lastInsertId();
    }

    /**
     * @param array<string, mixed> $data
     */
    public function update(int $id, array $data): void
    {
        $sets = [];
        $params = ['id' => $id];

        $allowedFields = [
            'shipment_id', 'tracking_number', 'status', 'command_id',
            'label_path', 'error_message', 'delivery_status', 'delivery_status_raw',
        ];

        foreach ($allowedFields as $field) {
            if (array_key_exists($field, $data)) {
                $sets[] = "$field = :$field";
                $params[$field] = $data[$field];
            }
        }

        if (array_key_exists('payload_json', $data)) {
            $sets[] = 'payload_json = :payload_json';
            $params['payload_json'] = is_array($data['payload_json'])
                ? json_encode($data['payload_json'], JSON_UNESCAPED_UNICODE)
                : $data['payload_json'];
        }

        if ($sets === []) {
            return;
        }

        $sets[] = 'updated_at = NOW()';
        $sql = 'UPDATE shipment_packages SET ' . implode(', ', $sets) . ' WHERE id = :id';
        $statement = $this->pdo->prepare($sql);
        $statement->execute($params);
    }

    /**
     * @return array<string, mixed>|null
     */
    public function findById(int $id): ?array
    {
        $statement = $this->pdo->prepare('SELECT * FROM shipment_packages WHERE id = :id LIMIT 1');
        $statement->execute(['id' => $id]);
        $row = $statement->fetch(PDO::FETCH_ASSOC);
        return is_array($row) ? $row : null;
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    public function findByOrderId(int $orderId): array
    {
        $statement = $this->pdo->prepare(
            'SELECT * FROM shipment_packages WHERE order_id = :order_id ORDER BY created_at DESC'
        );
        $statement->execute(['order_id' => $orderId]);
        return $statement->fetchAll(PDO::FETCH_ASSOC) ?: [];
    }

    /**
     * @return array<string, mixed>|null
     */
    public function findLatestByOrderId(int $orderId): ?array
    {
        $statement = $this->pdo->prepare(
            'SELECT * FROM shipment_packages WHERE order_id = :order_id ORDER BY created_at DESC, id DESC LIMIT 1'
        );
        $statement->execute(['order_id' => $orderId]);
        $row = $statement->fetch(PDO::FETCH_ASSOC);

        return is_array($row) ? $row : null;
    }

    /**
     * @return array<string, mixed>|null
     */
    public function createManual(int $orderId, string $trackingNumber, ?string $carrierName = null): int
    {
        $statement = $this->pdo->prepare(
            'INSERT INTO shipment_packages (order_id, provider, tracking_number, carrier_id, status)
             VALUES (:order_id, :provider, :tracking_number, :carrier_id, :status)'
        );

        $statement->execute([
            'order_id' => $orderId,
            'provider' => 'manual',
            'tracking_number' => $trackingNumber,
            'carrier_id' => $carrierName !== null && trim($carrierName) !== '' ? trim($carrierName) : null,
            'status' => 'created',
        ]);

        return (int) $this->pdo->lastInsertId();
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    public function findActiveForTracking(): array
    {
        $statement = $this->pdo->prepare(
            "SELECT * FROM shipment_packages
             WHERE provider != 'manual'
               AND delivery_status NOT IN ('delivered', 'returned', 'cancelled')
               AND status IN ('created', 'label_ready')
             ORDER BY delivery_status_updated_at ASC
             LIMIT 50"
        );
        $statement->execute();
        return $statement->fetchAll(PDO::FETCH_ASSOC) ?: [];
    }

    public function updateDeliveryStatus(int $id, string $deliveryStatus, ?string $deliveryStatusRaw): void
    {
        $statement = $this->pdo->prepare(
            'UPDATE shipment_packages
             SET delivery_status = :delivery_status,
                 delivery_status_raw = :delivery_status_raw,
                 delivery_status_updated_at = NOW(),
                 updated_at = NOW()
             WHERE id = :id'
        );
        $statement->execute([
            'id' => $id,
            'delivery_status' => $deliveryStatus,
            'delivery_status_raw' => $deliveryStatusRaw,
        ]);
    }

    public function findByCommandId(string $commandId): ?array
    {
        $statement = $this->pdo->prepare('SELECT * FROM shipment_packages WHERE command_id = :command_id LIMIT 1');
        $statement->execute(['command_id' => $commandId]);
        $row = $statement->fetch(PDO::FETCH_ASSOC);
        return is_array($row) ? $row : null;
    }

    public function resolveCarrierName(string $provider, string $serviceId): ?string
    {
        if ($serviceId === '') {
            return null;
        }

        $statement = $this->pdo->prepare(
            'SELECT provider_service_name FROM carrier_delivery_method_mappings
             WHERE provider = :provider AND provider_service_id = :service_id
             LIMIT 1'
        );
        $statement->execute(['provider' => $provider, 'service_id' => $serviceId]);
        $row = $statement->fetch(PDO::FETCH_ASSOC);
        $name = trim((string) ($row['provider_service_name'] ?? ''));

        return $name !== '' ? $name : null;
    }

    public function delete(int $id): void
    {
        $statement = $this->pdo->prepare('DELETE FROM shipment_packages WHERE id = :id');
        $statement->execute(['id' => $id]);
    }

    /**
     * Lista paczek klienta (match po email/phone/name OR) ze statusem dostawy "returned"
     * z wykluczeniem biezacego zamowienia. Uzywane do banera ostrzegawczego.
     *
     * @param array{email?:string,phone?:string,name?:string} $customer
     * @return array<int, array{order_id:int,internal_order_number:string,ordered_at:string,tracking_number:string,provider:string,delivery_status_raw:string}>
     */
    public function findReturnedByCustomer(array $customer, int $excludeOrderId, int $limit = 10): array
    {
        $email = trim((string) ($customer['email'] ?? ''));
        $phone = trim((string) ($customer['phone'] ?? ''));
        $name = trim((string) ($customer['name'] ?? ''));
        $phoneDigits = preg_replace('/[^0-9]+/', '', $phone) ?? '';

        if ($email === '' && strlen($phoneDigits) < 6 && $name === '') {
            return [];
        }

        $limit = max(1, min($limit, 50));

        $sql = 'SELECT sp.order_id,
                o.internal_order_number,
                COALESCE(o.ordered_at, o.source_created_at, o.fetched_at) AS ordered_at,
                COALESCE(sp.tracking_number, "") AS tracking_number,
                COALESCE(sp.provider, "") AS provider,
                COALESCE(sp.delivery_status_raw, "") AS delivery_status_raw
            FROM shipment_packages sp
            INNER JOIN orders o ON o.id = sp.order_id
            INNER JOIN order_addresses a2 ON a2.order_id = sp.order_id AND a2.address_type = "customer"
            WHERE sp.delivery_status = "returned"
              AND sp.order_id != :exclude_id
              AND (
                (:email <> "" AND LOWER(TRIM(a2.email)) = LOWER(TRIM(:email)))
                OR
                (LENGTH(:phone_digits) >= 6
                   AND REGEXP_REPLACE(a2.phone, "[^0-9]+", "") = :phone_digits)
                OR
                (:name <> "" AND LOWER(TRIM(a2.name)) = LOWER(TRIM(:name)))
              )
            GROUP BY sp.order_id
            ORDER BY ordered_at DESC
            LIMIT :lim';

        try {
            $statement = $this->pdo->prepare($sql);
            $statement->bindValue(':exclude_id', $excludeOrderId, PDO::PARAM_INT);
            $statement->bindValue(':email', $email, PDO::PARAM_STR);
            $statement->bindValue(':phone_digits', $phoneDigits, PDO::PARAM_STR);
            $statement->bindValue(':name', $name, PDO::PARAM_STR);
            $statement->bindValue(':lim', $limit, PDO::PARAM_INT);
            $statement->execute();
            $rows = $statement->fetchAll(PDO::FETCH_ASSOC);
        } catch (\Throwable) {
            return [];
        }

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

        return array_map(static function (array $row): array {
            return [
                'order_id' => (int) ($row['order_id'] ?? 0),
                'internal_order_number' => (string) ($row['internal_order_number'] ?? ''),
                'ordered_at' => (string) ($row['ordered_at'] ?? ''),
                'tracking_number' => (string) ($row['tracking_number'] ?? ''),
                'provider' => (string) ($row['provider'] ?? ''),
                'delivery_status_raw' => (string) ($row['delivery_status_raw'] ?? ''),
            ];
        }, $rows);
    }

    private function nullStr(string $value): ?string
    {
        $trimmed = trim($value);
        return $trimmed === '' ? null : $trimmed;
    }
}
