<?php
declare(strict_types=1);

namespace App\Modules\Settings;

use App\Core\Support\StringHelper;
use DateTimeImmutable;
use PDO;
use Throwable;

final class ShopproOrderSyncStateRepository
{
    private ?array $columns = null;

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

    /**
     * @return array{
     *   last_synced_updated_at:?string,
     *   last_synced_source_order_id:?string,
     *   last_run_at:?string,
     *   last_success_at:?string,
     *   last_error:?string
     * }
     */
    public function getState(int $integrationId): array
    {
        $default = $this->defaultState();
        if ($integrationId <= 0) {
            return $default;
        }

        $columns = $this->resolveColumns();
        if (!$columns['has_table']) {
            return $default;
        }

        $updatedAtColumn = $columns['updated_at_column'];
        $sourceOrderIdColumn = $columns['source_order_id_column'];
        if ($updatedAtColumn === null || $sourceOrderIdColumn === null) {
            return $default;
        }

        $selectParts = [
            $updatedAtColumn . ' AS last_synced_updated_at',
            $sourceOrderIdColumn . ' AS last_synced_source_order_id',
            'last_run_at',
            $columns['has_last_success_at'] ? 'last_success_at' : 'NULL AS last_success_at',
            'last_error',
        ];

        try {
            $statement = $this->pdo->prepare(
                'SELECT ' . implode(', ', $selectParts) . '
                FROM integration_order_sync_state
                WHERE integration_id = :integration_id
                LIMIT 1'
            );
            $statement->execute(['integration_id' => $integrationId]);
            $row = $statement->fetch(PDO::FETCH_ASSOC);
        } catch (Throwable) {
            return $default;
        }

        if (!is_array($row)) {
            return $default;
        }

        return [
            'last_synced_updated_at' => StringHelper::nullableString((string) ($row['last_synced_updated_at'] ?? '')),
            'last_synced_source_order_id' => StringHelper::nullableString((string) ($row['last_synced_source_order_id'] ?? '')),
            'last_run_at' => StringHelper::nullableString((string) ($row['last_run_at'] ?? '')),
            'last_success_at' => StringHelper::nullableString((string) ($row['last_success_at'] ?? '')),
            'last_error' => StringHelper::nullableString((string) ($row['last_error'] ?? '')),
        ];
    }

    public function markRunStarted(int $integrationId, DateTimeImmutable $now): void
    {
        $this->upsertState($integrationId, [
            'last_run_at' => $now->format('Y-m-d H:i:s'),
        ]);
    }

    public function markRunFailed(int $integrationId, DateTimeImmutable $now, string $error): void
    {
        $this->upsertState($integrationId, [
            'last_run_at' => $now->format('Y-m-d H:i:s'),
            'last_error' => mb_substr(trim($error), 0, 500),
        ]);
    }

    public function markRunSuccess(
        int $integrationId,
        DateTimeImmutable $now,
        ?string $lastSyncedUpdatedAt,
        ?string $lastSyncedSourceOrderId
    ): void {
        $changes = [
            'last_run_at' => $now->format('Y-m-d H:i:s'),
            'last_error' => null,
        ];

        if ($lastSyncedUpdatedAt !== null) {
            $changes['last_synced_updated_at'] = $lastSyncedUpdatedAt;
        }
        if ($lastSyncedSourceOrderId !== null) {
            $changes['last_synced_source_order_id'] = $lastSyncedSourceOrderId;
        }

        $this->upsertState($integrationId, $changes, true);
    }

    /**
     * @param array<string, mixed> $changes
     */
    private function upsertState(int $integrationId, array $changes, bool $setSuccessAt = false): void
    {
        if ($integrationId <= 0) {
            return;
        }

        $columns = $this->resolveColumns();
        if (!$columns['has_table']) {
            return;
        }

        $updatedAtColumn = $columns['updated_at_column'];
        $sourceOrderIdColumn = $columns['source_order_id_column'];
        if ($updatedAtColumn === null || $sourceOrderIdColumn === null) {
            return;
        }

        $insertColumns = ['integration_id', 'created_at', 'updated_at'];
        $insertValues = [':integration_id', ':created_at', ':updated_at'];
        $updateParts = ['updated_at = VALUES(updated_at)'];
        $params = [
            'integration_id' => $integrationId,
            'created_at' => date('Y-m-d H:i:s'),
            'updated_at' => date('Y-m-d H:i:s'),
        ];

        $columnMap = [
            'last_run_at' => 'last_run_at',
            'last_error' => 'last_error',
            'last_synced_updated_at' => $updatedAtColumn,
            'last_synced_source_order_id' => $sourceOrderIdColumn,
        ];

        if ($columns['has_last_status_pushed_at'] && array_key_exists('last_status_pushed_at', $changes)) {
            $columnMap['last_status_pushed_at'] = 'last_status_pushed_at';
        }

        foreach ($columnMap as $inputKey => $columnName) {
            if (!array_key_exists($inputKey, $changes)) {
                continue;
            }

            $paramName = $inputKey;
            $insertColumns[] = $columnName;
            $insertValues[] = ':' . $paramName;
            $updateParts[] = $columnName . ' = VALUES(' . $columnName . ')';
            $params[$paramName] = $changes[$inputKey];
        }

        if ($setSuccessAt && $columns['has_last_success_at']) {
            $insertColumns[] = 'last_success_at';
            $insertValues[] = ':last_success_at';
            $updateParts[] = 'last_success_at = VALUES(last_success_at)';
            $params['last_success_at'] = date('Y-m-d H:i:s');
        }

        try {
            $statement = $this->pdo->prepare(
                'INSERT INTO integration_order_sync_state (' . implode(', ', $insertColumns) . ')
                VALUES (' . implode(', ', $insertValues) . ')
                ON DUPLICATE KEY UPDATE ' . implode(', ', $updateParts)
            );
            $statement->execute($params);
        } catch (Throwable) {
            return;
        }
    }

    public function getLastStatusPushedAt(int $integrationId): ?string
    {
        if ($integrationId <= 0) {
            return null;
        }

        $columns = $this->resolveColumns();
        if (!$columns['has_table'] || !$columns['has_last_status_pushed_at']) {
            return null;
        }

        try {
            $statement = $this->pdo->prepare(
                'SELECT last_status_pushed_at
                FROM integration_order_sync_state
                WHERE integration_id = :integration_id
                LIMIT 1'
            );
            $statement->execute(['integration_id' => $integrationId]);
            $value = $statement->fetchColumn();

            if (!is_string($value) || trim($value) === '') {
                return null;
            }

            return trim($value);
        } catch (Throwable) {
            return null;
        }
    }

    public function updateLastStatusPushedAt(int $integrationId, string $datetime): void
    {
        $this->upsertState($integrationId, [
            'last_status_pushed_at' => $datetime,
        ]);
    }

    /**
     * @return array{
     *   has_table:bool,
     *   updated_at_column:?string,
     *   source_order_id_column:?string,
     *   has_last_success_at:bool
     * }
     */
    private function resolveColumns(): array
    {
        if ($this->columns !== null) {
            return $this->columns;
        }

        $result = [
            'has_table' => false,
            'updated_at_column' => null,
            'source_order_id_column' => null,
            'has_last_success_at' => false,
            'has_last_status_pushed_at' => false,
        ];

        try {
            $statement = $this->pdo->prepare(
                'SELECT COLUMN_NAME
                FROM information_schema.COLUMNS
                WHERE TABLE_SCHEMA = DATABASE()
                  AND TABLE_NAME = "integration_order_sync_state"'
            );
            $statement->execute();
            $rows = $statement->fetchAll(PDO::FETCH_COLUMN);
        } catch (Throwable) {
            $this->columns = $result;
            return $result;
        }

        if (!is_array($rows) || $rows === []) {
            $this->columns = $result;
            return $result;
        }

        $available = [];
        foreach ($rows as $columnName) {
            $name = trim((string) $columnName);
            if ($name === '') {
                continue;
            }
            $available[$name] = true;
        }

        $result['has_table'] = true;
        if (isset($available['last_synced_order_updated_at'])) {
            $result['updated_at_column'] = 'last_synced_order_updated_at';
        } elseif (isset($available['last_synced_external_updated_at'])) {
            $result['updated_at_column'] = 'last_synced_external_updated_at';
        }

        if (isset($available['last_synced_source_order_id'])) {
            $result['source_order_id_column'] = 'last_synced_source_order_id';
        } elseif (isset($available['last_synced_external_order_id'])) {
            $result['source_order_id_column'] = 'last_synced_external_order_id';
        }

        $result['has_last_success_at'] = isset($available['last_success_at']);
        $result['has_last_status_pushed_at'] = isset($available['last_status_pushed_at']);
        $this->columns = $result;

        return $result;
    }

    /**
     * @return array{
     *   last_synced_updated_at:?string,
     *   last_synced_source_order_id:?string,
     *   last_run_at:?string,
     *   last_success_at:?string,
     *   last_error:?string
     * }
     */
    private function defaultState(): array
    {
        return [
            'last_synced_updated_at' => null,
            'last_synced_source_order_id' => null,
            'last_run_at' => null,
            'last_success_at' => null,
            'last_error' => null,
        ];
    }
}
