<?php

namespace classes;

use objects\department_customer_price_overrides_o;
use objects\departments_o;
use objects\products_o;
use objects\users_o;

class department_customer_pricing_service
{
    /**
     * @return array<string, mixed>
     */
    public function getPricing(int $departmentId, int $userId): array
    {
        $department = $this->department($departmentId);
        $customer = $this->customer($userId);
        $this->assertEnabled($department);

        $overrides = (new department_customer_price_overrides_o())->getAllPrices($departmentId, $userId);

        return [
            'department' => $department,
            'customer' => $customer,
            'overrides' => $overrides,
            'categories' => $this->catalog($departmentId, $customer['id']),
        ];
    }

    /**
     * @param array<string, mixed> $payload
     * @return array<string, mixed>
     */
    public function updatePricing(int $departmentId, int $userId, array $payload): array
    {
        $department = $this->department($departmentId);
        $customer = $this->customer($userId);
        $this->assertEnabled($department);

        if (array_key_exists('department_id', $payload) && (int)$payload['department_id'] !== $departmentId) {
            throw new limited_backoffice_exception('Department ID in body does not match the route.', 400);
        }
        if (array_key_exists('user_id', $payload) && (int)$payload['user_id'] !== $userId) {
            throw new limited_backoffice_exception('User ID in body does not match the route.', 400);
        }

        $overrides = $payload['overrides'] ?? null;
        if (!is_array($overrides)) {
            throw new limited_backoffice_exception('Overrides are required.', 400);
        }

        $normalized = $this->normalizeOverrides($departmentId, $overrides);
        $overrideObject = new department_customer_price_overrides_o();
        $existingOverrides = $overrideObject->getAllPrices($departmentId, $customer['id']);
        $normalizedKeys = [];
        foreach ($normalized as $override) {
            $normalizedKeys[$this->overrideKey((bool)$override['is_category'], $override['product_or_category_id'])] = true;
        }

        global $db;
        $db->conn()->begin_transaction();
        try {
            $db->query(
                'DELETE FROM `department_customer_price_overrides` WHERE `department_id` = '
                . (int)$departmentId . ' AND `user_id` = ' . (int)$customer['id']
            );

            foreach ($normalized as $override) {
                $overrideObject->setPrice(
                    $departmentId,
                    $customer['id'],
                    (bool)$override['is_category'],
                    $override['product_or_category_id'],
                    (int)$override['percentage'],
                    $override['fixed_price']
                );
            }

            $db->conn()->commit();
        } catch (\Throwable) {
            $db->conn()->rollback();
            throw new limited_backoffice_exception('Unable to update department customer pricing.', 500);
        }

        foreach ($normalized as $override) {
            $this->recordVersion($customer, $departmentId, $override);
        }

        foreach ($existingOverrides as $existingOverride) {
            $key = $this->overrideKey((bool)$existingOverride['is_category'], $existingOverride['product_or_category_id']);
            if (isset($normalizedKeys[$key])) {
                continue;
            }

            $this->recordVersion($customer, $departmentId, [
                'is_category' => (bool)$existingOverride['is_category'],
                'product_or_category_id' => $existingOverride['product_or_category_id'],
                'percentage' => 0,
                'fixed_price' => null,
            ]);
        }

        return $this->getPricing($departmentId, $customer['id']);
    }

    /**
     * @return array{id:int,name:string,description:string,custom_pricing_only:bool}
     */
    private function department(int $departmentId): array
    {
        $department = (new departments_o())->getDepartmentById($departmentId);
        if (!is_array($department) || empty($department)) {
            throw new limited_backoffice_exception('Department not found', 404);
        }

        return [
            'id' => (int)$department['id'],
            'name' => (string)$department['name'],
            'description' => (string)($department['description'] ?? ''),
            'custom_pricing_only' => (bool)(int)($department['custom_pricing_only'] ?? 0),
        ];
    }

    /**
     * @return array{id:int,customer_number:int,display_name:string}
     */
    private function customer(int $userId): array
    {
        $customer = (new users_o())->getUserById($userId);
        if (!$customer->exists()) {
            throw new limited_backoffice_exception('Customer not found', 404);
        }

        return [
            'id' => (int)$customer->id,
            'customer_number' => (int)$customer->customer_number->value(),
            'display_name' => (string)($customer->display_name->value() ?: ('Customer #' . $customer->customer_number->value())),
        ];
    }

    /**
     * @param array<string, mixed> $department
     */
    private function assertEnabled(array $department): void
    {
        if (!($department['custom_pricing_only'] ?? false)) {
            throw new limited_backoffice_exception('Department customer pricing is disabled.', 409, [
                'message' => 'Department customer pricing is disabled.',
                'code' => 'department_customer_pricing_disabled',
                'department' => $department,
            ]);
        }
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function catalog(int $departmentId, int $userId): array
    {
        global $db;

        $sql = "
            SELECT
                c.`id` AS `category_id`,
                c.`name` AS `category_name`,
                c.`description` AS `category_description`,
                p.*,
                pdp.`price` AS `department_price`
            FROM `department_categories` dc
            INNER JOIN `categories` c ON c.`id` = dc.`category_id`
            INNER JOIN `products` p ON p.`category` = dc.`category_id`
            LEFT JOIN `product_department_prices` pdp
                ON pdp.`department_id` = dc.`department_id`
               AND pdp.`product_id` = p.`id`
            WHERE dc.`department_id` = " . (int)$departmentId . "
              AND dc.`deleted_at` IS NULL
            ORDER BY c.`name` ASC, c.`id` ASC, p.`order_priority` ASC, p.`name` ASC, p.`id` ASC";

        $result = $db->query($sql);
        $rows = $result ? $db->fetch_all($result) : [];
        $customer = (new users_o())->getUserById($userId);
        $categories = [];
        $seen = [];

        foreach ($rows as $row) {
            $productId = (int)$row['id'];
            if (isset($seen[$productId])) {
                continue;
            }
            $seen[$productId] = true;

            $categoryId = (int)$row['category_id'];
            if (!isset($categories[$categoryId])) {
                $categories[$categoryId] = [
                    'id' => $categoryId,
                    'name' => (string)$row['category_name'],
                    'description' => (string)($row['category_description'] ?? ''),
                    'products' => [],
                ];
            }

            $departmentPrice = $row['department_price'] === null ? null : (int)$row['department_price'];
            $effectivePrice = products_o::CUSTOM_PRICING_MISSING_PRICE;
            if ($departmentPrice !== null) {
                $effectivePrice = $customer->applyProductCustomerPricing($productId, $departmentPrice, true, $departmentId);
            }

            $categories[$categoryId]['products'][] = [
                'id' => $productId,
                'name' => (string)$row['name'],
                'description' => (string)($row['description'] ?? ''),
                'category' => $categoryId,
                'apply_category_discount' => (bool)$row['apply_category_discount'],
                'base_price' => (int)$row['price'],
                'department_price' => $departmentPrice,
                'effective_price' => $effectivePrice,
                'missing_department_price' => $departmentPrice === null,
            ];
        }

        return array_values($categories);
    }

    /**
     * @param array<int, mixed> $overrides
     * @return array<int, array{is_category:bool,product_or_category_id:int|string,percentage:int,fixed_price:int|null}>
     */
    private function normalizeOverrides(int $departmentId, array $overrides): array
    {
        $normalized = [];
        foreach ($overrides as $override) {
            if (!is_array($override)) {
                throw new limited_backoffice_exception('Invalid override payload.', 400);
            }

            $isCategory = (bool)($override['is_category'] ?? false);
            $objectId = $override['product_or_category_id'] ?? $override['object_id'] ?? null;
            if ($objectId === null || $objectId === '') {
                throw new limited_backoffice_exception('Override object is required.', 400);
            }

            $percentage = filter_var($override['discount'] ?? $override['percentage'] ?? 0, FILTER_VALIDATE_INT);
            if ($percentage === false || $percentage < 0 || $percentage > 100) {
                throw new limited_backoffice_exception('Discount must be between 0 and 100.', 400);
            }

            $fixedPrice = null;
            if (array_key_exists('fixed_price', $override) && $override['fixed_price'] !== null && $override['fixed_price'] !== '') {
                $fixedPrice = filter_var($override['fixed_price'], FILTER_VALIDATE_INT);
                if ($fixedPrice === false || $fixedPrice < 0) {
                    throw new limited_backoffice_exception('Fixed price must be zero or more.', 400);
                }
            }

            if ($isCategory) {
                $fixedPrice = null;
                $objectId = (string)$objectId;
                if ($objectId !== 'global') {
                    $this->assertDepartmentCategory($departmentId, $objectId);
                }
            } else {
                $objectId = (int)$objectId;
                $this->assertDepartmentProduct($departmentId, $objectId);
            }

            if ($percentage <= 0 && $fixedPrice === null) {
                continue;
            }

            $key = $this->overrideKey($isCategory, $objectId);
            $normalized[$key] = [
                'is_category' => $isCategory,
                'product_or_category_id' => $objectId,
                'percentage' => (int)$percentage,
                'fixed_price' => $fixedPrice === null ? null : (int)$fixedPrice,
            ];
        }

        return array_values($normalized);
    }

    /**
     * @param array{id:int,customer_number:int,display_name:string} $customer
     * @param array{is_category:bool,product_or_category_id:int|string,percentage:int,fixed_price:int|null} $override
     */
    private function recordVersion(array $customer, int $departmentId, array $override): void
    {
        try {
            (new economic_v2_versioning_service())->recordDiscountOverrideVersion(
                (int)$customer['id'],
                (int)$customer['customer_number'],
                (bool)$override['is_category'],
                (string)$override['product_or_category_id'],
                (int)$override['percentage'],
                date('Y-m-d H:i:s'),
                'live.department_discount_override.route',
                1.0,
                false,
                [
                    'route' => 'department_customer_pricing',
                    'department_id' => $departmentId,
                ],
                $override['fixed_price'],
                $departmentId
            );
        } catch (\Throwable) {
        }
    }

    private function overrideKey(bool $isCategory, int|string $objectId): string
    {
        return ((int)$isCategory) . ':' . (string)$objectId;
    }

    private function assertDepartmentProduct(int $departmentId, int $productId): void
    {
        global $db;

        $result = $db->query(
            'SELECT p.`id`
             FROM `department_categories` dc
             INNER JOIN `products` p ON p.`category` = dc.`category_id`
             WHERE dc.`department_id` = ' . (int)$departmentId . '
               AND dc.`deleted_at` IS NULL
               AND p.`id` = ' . (int)$productId . '
             LIMIT 1'
        );

        if (!$result || $result->num_rows < 1) {
            throw new limited_backoffice_exception('Product is not available for this department.', 400);
        }
    }

    private function assertDepartmentCategory(int $departmentId, string $categoryId): void
    {
        global $db;

        $categoryId = $db->escape_string($categoryId);
        $result = $db->query(
            "SELECT `id`
             FROM `department_categories`
             WHERE `department_id` = " . (int)$departmentId . "
               AND `deleted_at` IS NULL
               AND `category_id` = '{$categoryId}'
             LIMIT 1"
        );

        if (!$result || $result->num_rows < 1) {
            throw new limited_backoffice_exception('Category is not available for this department.', 400);
        }
    }
}
