<?php

namespace classes;

use DateTime;
use Exception;

class economic_v2_versioning_service
{
    public function __construct()
    {
        economic_v2_schema_bootstrap::ensureTables();
    }

    public function recordFixedPricingVersion(
        int $customer_number,
        ?int $price,
        ?string $description,
        ?string $effective_from = null,
        string $source = 'live.fixed_pricing',
        float $confidence = 1.0,
        bool $inferred = false,
        array $metadata = []
    ): array {
        if ($price === null) {
            return $this->closeActiveFixedPricingVersion(
                $customer_number,
                $effective_from,
                $source,
                $confidence,
                $inferred,
                $metadata
            );
        }

        return $this->upsertVersion(
            'customer_fixed_pricing_versions',
            [
                'customer_number' => $customer_number,
            ],
            [
                'price' => (int)$price,
                'description' => $description ?? '',
            ],
            $this->normalizeDatetime($effective_from),
            $source,
            $confidence,
            $inferred,
            $metadata
        );
    }

    public function closeActiveFixedPricingVersion(
        int $customer_number,
        ?string $effective_to = null,
        string $source = 'live.fixed_pricing',
        float $confidence = 1.0,
        bool $inferred = false,
        array $metadata = []
    ): array {
        return $this->closeActiveVersion(
            'customer_fixed_pricing_versions',
            [
                'customer_number' => $customer_number,
            ],
            $this->normalizeDatetime($effective_to),
            $source,
            $confidence,
            $inferred,
            $metadata
        );
    }

    public function recordVehicleSubscriptionVersion(
        array $state,
        ?string $effective_from = null,
        string $source = 'live.vehicle',
        float $confidence = 1.0,
        bool $inferred = false,
        array $metadata = []
    ): array {
        if (!isset($state['customer_number'], $state['reg'], $state['vehicle_type'], $state['wash_subscription'])) {
            throw new Exception('Missing required vehicle version state keys');
        }

        return $this->upsertVersion(
            'customer_vehicle_subscription_versions',
            [
                'customer_number' => (int)$state['customer_number'],
                'reg' => (string)$state['reg'],
            ],
            [
                'vehicle_id' => isset($state['vehicle_id']) ? (int)$state['vehicle_id'] : null,
                'vehicle_type' => (int)$state['vehicle_type'],
                'wash_subscription' => (int)((bool)$state['wash_subscription']),
            ],
            $this->normalizeDatetime($effective_from),
            $source,
            $confidence,
            $inferred,
            $metadata
        );
    }

    public function closeActiveVehicleSubscriptionVersion(
        int $customer_number,
        string $reg,
        ?string $effective_to = null,
        string $source = 'live.vehicle',
        float $confidence = 1.0,
        bool $inferred = false,
        array $metadata = []
    ): array {
        return $this->closeActiveVersion(
            'customer_vehicle_subscription_versions',
            [
                'customer_number' => $customer_number,
                'reg' => $reg,
            ],
            $this->normalizeDatetime($effective_to),
            $source,
            $confidence,
            $inferred,
            $metadata
        );
    }

    public function recordDiscountOverrideVersion(
        int $user_id,
        int $customer_number,
        bool $is_category,
        int|string $object_id,
        ?int $discount,
        ?string $effective_from = null,
        string $source = 'live.discount_override',
        float $confidence = 1.0,
        bool $inferred = false,
        array $metadata = [],
        ?int $fixed_price = null,
        ?int $department_id = null
    ): array {
        $identity = [
            'user_id' => $user_id,
            'customer_number' => $customer_number,
            'is_category' => (int)$is_category,
            'object_id' => (string)$object_id,
        ];
        if ($department_id !== null) {
            $identity['department_id'] = (int)$department_id;
        }

        if (($discount === null || (int)$discount === 0) && $fixed_price === null) {
            return $this->closeActiveVersion(
                'customer_discount_override_versions',
                $identity,
                $this->normalizeDatetime($effective_from),
                $source,
                $confidence,
                $inferred,
                $metadata
            );
        }

        return $this->upsertVersion(
            'customer_discount_override_versions',
            $identity,
            [
                'discount' => (int)$discount,
                'fixed_price' => $is_category ? null : $fixed_price,
            ],
            $this->normalizeDatetime($effective_from),
            $source,
            $confidence,
            $inferred,
            $metadata
        );
    }

    public function listFixedPricingVersions(int $customer_number, ?string $date_from = null, ?string $date_to = null): array
    {
        return $this->listVersions(
            'customer_fixed_pricing_versions',
            ['customer_number' => $customer_number],
            $date_from,
            $date_to
        );
    }

    public function listVehicleSubscriptionVersions(int $customer_number, ?string $date_from = null, ?string $date_to = null): array
    {
        return $this->listVersions(
            'customer_vehicle_subscription_versions',
            ['customer_number' => $customer_number],
            $date_from,
            $date_to
        );
    }

    public function listDiscountOverrideVersions(int $customer_number, ?string $date_from = null, ?string $date_to = null): array
    {
        return $this->listVersions(
            'customer_discount_override_versions',
            ['customer_number' => $customer_number],
            $date_from,
            $date_to
        );
    }

    public function resolveFixedPricingVersionAt(int $customer_number, string $timestamp): ?array
    {
        $rows = $this->resolveActiveVersions(
            'customer_fixed_pricing_versions',
            ['customer_number' => $customer_number],
            $timestamp,
            'effective_from DESC, id DESC',
            1
        );
        return $rows[0] ?? null;
    }

    public function resolveVehicleSubscriptionVersionsAt(int $customer_number, string $timestamp): array
    {
        $rows = $this->resolveActiveVersions(
            'customer_vehicle_subscription_versions',
            [
                'customer_number' => $customer_number,
                'wash_subscription' => 1,
            ],
            $timestamp,
            'reg ASC, effective_from DESC, id DESC'
        );

        $unique = [];
        foreach ($rows as $row) {
            $reg = (string)$row['reg'];
            if (!isset($unique[$reg])) {
                $unique[$reg] = $row;
            }
        }
        return array_values($unique);
    }

    public function resolveDiscountOverrideAt(
        int $customer_number,
        bool $is_category,
        int|string $object_id,
        string $timestamp,
        ?int $department_id = null
    ): ?array {
        $identity = [
            'customer_number' => $customer_number,
            'is_category' => (int)$is_category,
            'object_id' => (string)$object_id,
        ];
        if ($department_id !== null) {
            $identity['department_id'] = (int)$department_id;
        }

        $rows = $this->resolveActiveVersions(
            'customer_discount_override_versions',
            $identity,
            $timestamp,
            'effective_from DESC, id DESC',
            1
        );
        return $rows[0] ?? null;
    }

    public function runBestEffortBackfill(): array
    {
        global $db;
        economic_v2_schema_bootstrap::ensureTables();

        $report = [
            'fixed_pricing' => ['inserted' => 0, 'updated' => 0, 'closed' => 0, 'noop' => 0],
            'vehicle_subscriptions' => ['inserted' => 0, 'updated' => 0, 'closed' => 0, 'noop' => 0],
            'discount_overrides' => ['inserted' => 0, 'updated' => 0, 'closed' => 0, 'noop' => 0],
            'inferred' => ['fixed_pricing' => 0, 'vehicle_subscriptions' => 0],
            'warnings' => [],
        ];

        // Fixed pricing current state.
        $has_fixed_created_at = economic_v2_schema_bootstrap::tableHasColumn('customer_fixed_pricing', 'created_at');
        $fixed_cols = $has_fixed_created_at
            ? 'customer_number, price, description, created_at'
            : 'customer_number, price, description';
        $fixed_rows = $this->fetchAll("SELECT $fixed_cols FROM customer_fixed_pricing");
        foreach ($fixed_rows as $row) {
            $effective_from = $has_fixed_created_at
                ? $this->normalizeDatetime((string)$row['created_at'])
                : $this->normalizeDatetime(null);
            $confidence = $has_fixed_created_at ? 0.8 : 0.6;
            $result = $this->recordFixedPricingVersion(
                (int)$row['customer_number'],
                (int)$row['price'],
                (string)($row['description'] ?? ''),
                $effective_from,
                'backfill.current_fixed_pricing',
                $confidence,
                true,
                ['table' => 'customer_fixed_pricing']
            );
            $this->incrementReportAction($report['fixed_pricing'], $result['action'] ?? 'noop');
        }

        // Infer fixed pricing start from synthetic fixed-price orders when no timeline exists.
        $fixed_inferred = $this->fetchAll(
            "SELECT o.customer_id AS customer_number, MIN(o.created_at) AS first_seen, MAX(oi.price) AS inferred_price
             FROM orders o
             JOIN order_items oi ON oi.order_id = o.id
             WHERE o.deleted_at IS NULL
               AND oi.deleted_at IS NULL
               AND o.reference = 'Fast pris aftale'
               AND oi.product_id = 61
             GROUP BY o.customer_id"
        );
        foreach ($fixed_inferred as $row) {
            $customer_number = (int)$row['customer_number'];
            if ($this->resolveFixedPricingVersionAt($customer_number, (string)$row['first_seen']) !== null) {
                continue;
            }
            $price = (int)($row['inferred_price'] ?? 0);
            if ($price <= 0) {
                continue;
            }
            $this->recordFixedPricingVersion(
                $customer_number,
                $price,
                'Inferred from fixed-pricing invoice order',
                $this->normalizeDatetime((string)$row['first_seen']),
                'backfill.inferred_fixed_pricing_order',
                0.55,
                true,
                ['reference' => 'Fast pris aftale', 'product_id' => 61]
            );
            $report['inferred']['fixed_pricing']++;
        }

        // Vehicle subscriptions current state.
        $has_vehicle_created_at = economic_v2_schema_bootstrap::tableHasColumn('customer_vehicles', 'created_at');
        $has_vehicle_deleted_at = economic_v2_schema_bootstrap::tableHasColumn('customer_vehicles', 'deleted_at');
        $vehicle_cols = 'id, customer_id, reg, type, wash_subscription' .
            ($has_vehicle_created_at ? ', created_at' : '') .
            ($has_vehicle_deleted_at ? ', deleted_at' : '');
        $vehicle_rows = $this->fetchAll("SELECT $vehicle_cols FROM customer_vehicles");
        foreach ($vehicle_rows as $row) {
            $effective_from = $has_vehicle_created_at
                ? $this->normalizeDatetime((string)$row['created_at'])
                : $this->normalizeDatetime(null);
            $confidence = $has_vehicle_created_at ? 0.75 : 0.55;
            $result = $this->recordVehicleSubscriptionVersion(
                [
                    'vehicle_id' => (int)$row['id'],
                    'customer_number' => (int)$row['customer_id'],
                    'reg' => (string)$row['reg'],
                    'vehicle_type' => (int)$row['type'],
                    'wash_subscription' => (bool)$row['wash_subscription'],
                ],
                $effective_from,
                'backfill.current_vehicle',
                $confidence,
                true,
                ['table' => 'customer_vehicles']
            );
            $this->incrementReportAction($report['vehicle_subscriptions'], $result['action'] ?? 'noop');

            if ($has_vehicle_deleted_at && !empty($row['deleted_at'])) {
                $close_result = $this->closeActiveVehicleSubscriptionVersion(
                    (int)$row['customer_id'],
                    (string)$row['reg'],
                    $this->normalizeDatetime((string)$row['deleted_at']),
                    'backfill.current_vehicle_deleted',
                    0.9,
                    true,
                    ['table' => 'customer_vehicles']
                );
                $this->incrementReportAction($report['vehicle_subscriptions'], $close_result['action'] ?? 'noop');
            }
        }

        // Infer subscriptions from synthetic subscription orders.
        $subscription_inferred = $this->fetchAll(
            "SELECT o.customer_id AS customer_number,
                    oi.reference AS reg,
                    oi.product_id AS vehicle_type,
                    MIN(o.created_at) AS first_seen
             FROM orders o
             JOIN order_items oi ON oi.order_id = o.id
             WHERE o.deleted_at IS NULL
               AND oi.deleted_at IS NULL
               AND o.reference = 'Vaskeabonnementer'
               AND oi.reference <> ''
               AND oi.quantity > 0
             GROUP BY o.customer_id, oi.reference, oi.product_id"
        );
        foreach ($subscription_inferred as $row) {
            $resolved = $this->resolveVehicleSubscriptionVersionsAt((int)$row['customer_number'], (string)$row['first_seen']);
            $already = false;
            foreach ($resolved as $active) {
                if ((string)$active['reg'] === (string)$row['reg']) {
                    $already = true;
                    break;
                }
            }
            if ($already) {
                continue;
            }
            $this->recordVehicleSubscriptionVersion(
                [
                    'vehicle_id' => null,
                    'customer_number' => (int)$row['customer_number'],
                    'reg' => (string)$row['reg'],
                    'vehicle_type' => (int)$row['vehicle_type'],
                    'wash_subscription' => true,
                ],
                $this->normalizeDatetime((string)$row['first_seen']),
                'backfill.inferred_subscription_order',
                0.5,
                true,
                ['reference' => 'Vaskeabonnementer']
            );
            $report['inferred']['vehicle_subscriptions']++;
        }

        // Discount overrides current state.
        price_overrides_schema_bootstrap::ensureColumns();
        $has_override_created_at = economic_v2_schema_bootstrap::tableHasColumn('price_overrides', 'created_at');
        $has_override_fixed_price = economic_v2_schema_bootstrap::tableHasColumn('price_overrides', 'fixed_price');
        $discount_cols = 'po.user_id, u.customer_number, po.is_category, po.product_or_category_id, po.percentage' .
            ($has_override_fixed_price ? ', po.fixed_price' : '') .
            ($has_override_created_at ? ', po.created_at' : '');
        $discount_rows = $this->fetchAll(
            "SELECT $discount_cols
             FROM price_overrides po
             JOIN users u ON u.id = po.user_id"
        );
        foreach ($discount_rows as $row) {
            $effective_from = $has_override_created_at
                ? $this->normalizeDatetime((string)$row['created_at'])
                : $this->normalizeDatetime(null);
            $confidence = $has_override_created_at ? 0.85 : 0.6;
            $result = $this->recordDiscountOverrideVersion(
                (int)$row['user_id'],
                (int)$row['customer_number'],
                (bool)$row['is_category'],
                (string)$row['product_or_category_id'],
                (int)$row['percentage'],
                $effective_from,
                'backfill.current_discount_override',
                $confidence,
                true,
                ['table' => 'price_overrides'],
                $has_override_fixed_price && $row['fixed_price'] !== null ? (int)$row['fixed_price'] : null
            );
            $this->incrementReportAction($report['discount_overrides'], $result['action'] ?? 'noop');
        }

        return $report;
    }

    private function upsertVersion(
        string $table,
        array $identity,
        array $values,
        string $effective_from,
        string $source,
        float $confidence,
        bool $inferred,
        array $metadata
    ): array {
        global $db;

        $confidence = $this->normalizeConfidence($confidence);
        $metadata_json = $db->escape_string(json_encode($metadata, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
        $source = $db->escape_string($source);
        $effective_from = $db->escape_string($effective_from);

        // Close the previous active interval when a new one starts.
        $close_to = $db->escape_string($this->minusOneSecond($effective_from));
        $identity_where = $this->buildWhereClause($identity);
        $db->query(
            "UPDATE $table
             SET effective_to = '$close_to'
             WHERE $identity_where
               AND effective_from < '$effective_from'
               AND (effective_to IS NULL OR effective_to >= '$effective_from')"
        );

        $existing = $this->fetchOne(
            "SELECT id
             FROM $table
             WHERE $identity_where
               AND effective_from = '$effective_from'
             ORDER BY id DESC
             LIMIT 1"
        );

        if ($existing !== null) {
            $id = (int)$existing['id'];
            $set_parts = [];
            foreach ($values as $k => $v) {
                $set_parts[] = $this->buildSetFragment($k, $v);
            }
            $set_parts[] = "source = '$source'";
            $set_parts[] = "confidence = $confidence";
            $set_parts[] = "inferred = " . ((int)$inferred);
            $set_parts[] = "metadata_json = '$metadata_json'";
            $db->query("UPDATE $table SET " . implode(', ', $set_parts) . " WHERE id = $id");
            return [
                'action' => 'updated',
                'row' => $this->fetchOne("SELECT * FROM $table WHERE id = $id"),
            ];
        }

        $next_start = $this->fetchOne(
            "SELECT effective_from
             FROM $table
             WHERE $identity_where
               AND effective_from > '$effective_from'
             ORDER BY effective_from ASC
             LIMIT 1"
        );
        $effective_to_value = null;
        if ($next_start !== null && !empty($next_start['effective_from'])) {
            $effective_to_value = $this->minusOneSecond((string)$next_start['effective_from']);
        }

        $insert_data = [
            ...$identity,
            ...$values,
            'effective_from' => $effective_from,
            'effective_to' => $effective_to_value,
            'source' => $source,
            'confidence' => $confidence,
            'inferred' => (int)$inferred,
            'metadata_json' => $metadata_json,
        ];

        $columns = [];
        $values_sql = [];
        foreach ($insert_data as $k => $v) {
            $columns[] = $k;
            $values_sql[] = $this->buildValueFragment($v);
        }

        $db->query(
            "INSERT INTO $table (" . implode(', ', $columns) . ")
             VALUES (" . implode(', ', $values_sql) . ")"
        );
        $id = (int)$db->insert_id();

        return [
            'action' => 'inserted',
            'row' => $this->fetchOne("SELECT * FROM $table WHERE id = $id"),
        ];
    }

    private function closeActiveVersion(
        string $table,
        array $identity,
        string $effective_to,
        string $source,
        float $confidence,
        bool $inferred,
        array $metadata
    ): array {
        global $db;

        $confidence = $this->normalizeConfidence($confidence);
        $source = $db->escape_string($source);
        $metadata_json = $db->escape_string(json_encode($metadata, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
        $effective_to = $db->escape_string($effective_to);
        $identity_where = $this->buildWhereClause($identity);

        $result = $db->query(
            "UPDATE $table
             SET effective_to = '$effective_to',
                 source = '$source',
                 confidence = $confidence,
                 inferred = " . ((int)$inferred) . ",
                 metadata_json = '$metadata_json'
             WHERE $identity_where
               AND effective_from <= '$effective_to'
               AND (effective_to IS NULL OR effective_to > '$effective_to')"
        );

        if ($result && $db->conn()->affected_rows > 0) {
            return ['action' => 'closed'];
        }

        return ['action' => 'noop'];
    }

    private function listVersions(string $table, array $identity, ?string $date_from, ?string $date_to): array
    {
        $where = $this->buildWhereClause($identity);
        if ($date_from !== null) {
            $date_from = $this->normalizeDatetime($date_from);
            $where .= " AND (effective_to IS NULL OR effective_to >= '" . $this->escape($date_from) . "')";
        }
        if ($date_to !== null) {
            $date_to = $this->normalizeDatetime($date_to);
            $where .= " AND effective_from <= '" . $this->escape($date_to) . "'";
        }
        return $this->fetchAll("SELECT * FROM $table WHERE $where ORDER BY effective_from ASC, id ASC");
    }

    private function resolveActiveVersions(
        string $table,
        array $identity,
        string $timestamp,
        string $order_by,
        ?int $limit = null
    ): array {
        $timestamp = $this->normalizeDatetime($timestamp);
        $where = $this->buildWhereClause($identity);
        $where .= " AND effective_from <= '" . $this->escape($timestamp) . "'";
        $where .= " AND (effective_to IS NULL OR effective_to >= '" . $this->escape($timestamp) . "')";
        $sql = "SELECT * FROM $table WHERE $where ORDER BY $order_by";
        if ($limit !== null) {
            $sql .= " LIMIT " . ((int)$limit);
        }
        return $this->fetchAll($sql);
    }

    private function buildWhereClause(array $identity): string
    {
        $parts = [];
        foreach ($identity as $k => $v) {
            if ($v === null) {
                $parts[] = "$k IS NULL";
                continue;
            }
            if (is_bool($v)) {
                $parts[] = "$k = " . ((int)$v);
                continue;
            }
            if (is_int($v) || is_float($v)) {
                $parts[] = "$k = $v";
                continue;
            }
            $parts[] = "$k = '" . $this->escape((string)$v) . "'";
        }
        return implode(' AND ', $parts);
    }

    private function buildSetFragment(string $key, mixed $value): string
    {
        return "$key = " . $this->buildValueFragment($value);
    }

    private function buildValueFragment(mixed $value): string
    {
        if ($value === null) {
            return 'NULL';
        }
        if (is_bool($value)) {
            return (string)((int)$value);
        }
        if (is_int($value) || is_float($value)) {
            return (string)$value;
        }
        return "'" . $this->escape((string)$value) . "'";
    }

    private function normalizeDatetime(?string $value): string
    {
        if ($value === null || trim($value) === '') {
            return date('Y-m-d H:i:s');
        }
        $dt = new DateTime($value);
        return $dt->format('Y-m-d H:i:s');
    }

    private function minusOneSecond(string $datetime): string
    {
        $dt = new DateTime($datetime);
        $dt->modify('-1 second');
        return $dt->format('Y-m-d H:i:s');
    }

    private function normalizeConfidence(float $confidence): float
    {
        if ($confidence < 0) {
            return 0.0;
        }
        if ($confidence > 1) {
            return 1.0;
        }
        return round($confidence, 5);
    }

    private function escape(string $value): string
    {
        global $db;
        return $db->escape_string($value);
    }

    private function fetchAll(string $sql): array
    {
        global $db;
        $result = $db->query($sql);
        if (!$result) {
            return [];
        }
        return $db->fetch_all($result);
    }

    private function fetchOne(string $sql): ?array
    {
        $rows = $this->fetchAll($sql);
        if (empty($rows)) {
            return null;
        }
        return $rows[0];
    }

    private function incrementReportAction(array &$bucket, string $action): void
    {
        if (!isset($bucket[$action])) {
            $bucket[$action] = 0;
        }
        $bucket[$action]++;
    }
}
