// SPDX-FileCopyrightText: 2024 Manuel Alcaraz Zambrano <manuel@alcarazzam.dev>
// SPDX-License-Identifier: GPL-3.0-or-later

#include "tournament.h"

#include <KLocalizedString>
#include <QSqlError>
#include <QSqlQuery>
#include <QSqlRecord>

#include <algorithm>

#include "db.h"
#include "event.h"
#include "state.h"
#include "tiebreaks/aob.h"
#include "tiebreaks/buchholz.h"
#include "tiebreaks/dummy.h"
#include "tiebreaks/numberwins.h"
#include "tiebreaks/playedblack.h"
#include "tiebreaks/points.h"
#include "tiebreaks/won.h"
#include "timecontrol.h"
#include "trf/reader.h"
#include "trf/writer.h"
#include "utils.h"

Tournament::Tournament(Event *event)
    : m_event(event)
    , m_timeControl({TimeControlPeriod{std::nullopt, 5400, 30}})
{
    m_tiebreaks.push_back(std::make_unique<Points>());
}

QString Tournament::id() const
{
    return m_id;
}

void Tournament::setId(const QString &id)
{
    if (m_id == id) {
        return;
    }
    m_id = id;
    Q_EMIT idChanged();
}

QString Tournament::name() const
{
    return m_name;
}

void Tournament::setName(const QString &name)
{
    if (m_name == name) {
        return;
    }
    m_name = name;
    setOption(u"name"_s, name);
    Q_EMIT nameChanged();
}

QString Tournament::city() const
{
    return m_city;
}

void Tournament::setCity(const QString &city)
{
    if (m_city == city) {
        return;
    }
    m_city = city;
    setOption(u"city"_s, city);
    Q_EMIT cityChanged();
}

QString Tournament::federation() const
{
    return m_federation;
}

void Tournament::setFederation(const QString &federation)
{
    if (m_federation == federation) {
        return;
    }
    m_federation = federation;
    setOption(u"federation"_s, federation);
    Q_EMIT federationChanged();
}

std::vector<std::unique_ptr<Arbiter>> &Tournament::arbiters()
{
    return m_arbiters;
}

void Tournament::saveArbiters()
{
    QJsonArray values;

    for (const auto &arbiter : m_arbiters) {
        values << arbiter->toJson();
    }

    const auto doc = QJsonDocument{QJsonObject{{"arbiters"_L1, values}}};
    const auto text = doc.toJson(QJsonDocument::Compact);

    setOption("arbiters"_L1, text);
}

TimeControl &Tournament::timeControl()
{
    return m_timeControl;
}

void Tournament::saveTimeControl()
{
    const auto text = QJsonDocument{m_timeControl.json()}.toJson(QJsonDocument::JsonFormat::Compact);

    qDebug() << m_timeControl.json();

    setOption(u"time_control"_s, text);
}

std::vector<std::unique_ptr<Tiebreak>> &Tournament::tiebreaks()
{
    return m_tiebreaks;
}

void Tournament::setTiebreaks(std::vector<std::unique_ptr<Tiebreak>> tiebreaks)
{
    if (m_tiebreaks == tiebreaks) {
        return;
    }
    m_tiebreaks = std::move(tiebreaks);
    saveTiebreaks();
}

std::expected<void, QString> Tournament::setTiebreaksFromTrf(const QString &line)
{
    std::vector<std::unique_ptr<Tiebreak>> tiebreaks;

    const auto codes = line.split(u',', Qt::SkipEmptyParts);

    for (const auto &code : codes) {
        auto tiebreak = tiebreakFromTrf(code);
        if (!tiebreak) {
            return std::unexpected(tiebreak.error());
        }
        if (tiebreak == nullptr) {
            continue;
        }
        tiebreaks.push_back(std::move(*tiebreak));
    }

    setTiebreaks(std::move(tiebreaks));

    return {};
}

int Tournament::numberOfRounds() const
{
    return m_numberOfRounds;
}

void Tournament::setNumberOfRounds(int numberOfRounds)
{
    if (m_numberOfRounds == numberOfRounds) {
        return;
    }
    m_numberOfRounds = numberOfRounds;
    setOption(u"number_of_rounds"_s, numberOfRounds);
    Q_EMIT numberOfRoundsChanged();
}

int Tournament::currentRound() const
{
    return m_currentRound;
}

void Tournament::setCurrentRound(int currentRound)
{
    Q_ASSERT(currentRound >= 0);
    if (m_currentRound == currentRound) {
        return;
    }
    m_currentRound = currentRound;
    setOption(u"current_round"_s, currentRound);
    Q_EMIT currentRoundChanged();
}

QList<Player *> Tournament::players() const
{
    QList<Player *> result;
    result.reserve(static_cast<qsizetype>(m_players.size()));

    for (const auto &player : std::as_const(m_players)) {
        result << player.get();
    }

    return result;
}

std::expected<void, QString> Tournament::addPlayer(std::unique_ptr<Player> player)
{
    m_event->db().transaction();

    player->setId(QUuid::createUuid().toString(QUuid::StringFormat::WithoutBraces));

    QSqlQuery query(m_event->db());
    query.prepare(ADD_PLAYER_QUERY);
    query.bindValue(u":id"_s, player->id());
    query.bindValue(u":startingRank"_s, player->startingRank());
    query.bindValue(u":title"_s, player->title());
    query.bindValue(u":name"_s, player->name());
    query.bindValue(u":rating"_s, player->rating());
    query.bindValue(u":nationalRating"_s, player->nationalRating());
    query.bindValue(u":playerId"_s, player->playerId());
    query.bindValue(u":nationalId"_s, player->nationalId());
    query.bindValue(u":birthDate"_s, player->birthDate());
    query.bindValue(u":federation"_s, player->federation());
    query.bindValue(u":origin"_s, player->origin());
    query.bindValue(u":gender"_s, player->gender());
    query.bindValue(u":extra"_s, player->extraString());
    query.bindValue(u":tournament"_s, m_id);

    if (!query.exec()) {
        qDebug() << "add player" << *player << query.lastError();
        return std::unexpected(query.lastError().text());
    }

    std::vector<std::unique_ptr<Pairing>> pairings;
    for (int i = 1; i <= m_currentRound; ++i) {
        const qsizetype board = this->pairings(i).size() + 1;

        auto pairing = std::make_unique<Pairing>(board, player.get(), nullptr, Pairing::PartialResult::ZeroBye, Pairing::PartialResult::Unknown);
        if (auto ok = savePairing(pairing.get(), i); !ok) {
            return std::unexpected(ok.error());
        }

        pairings.push_back(std::move(pairing));
    }

    if (!m_event->db().commit()) {
        return std::unexpected(m_event->db().lastError().text());
    }

    m_players.push_back(std::move(player));

    for (int i = 1; i <= m_currentRound; ++i) {
        const auto round = this->round(i);
        auto pairing = std::move(pairings.at(i - 1));
        round->addPairing(std::move(pairing));
    }

    Q_EMIT numberOfPlayersChanged();
    Q_EMIT numberOfRatedPlayersChanged();

    return {};
}

std::expected<void, QString> Tournament::deletePlayer(int startingRank)
{
    Q_ASSERT(m_currentRound == 0);

    const auto &player = m_players.at(startingRank - 1);

    if (const auto ok = deletePairings(player.get()); !ok) {
        return ok;
    }

    QSqlQuery query(m_event->db());
    query.prepare(DELETE_PLAYER_QUERY);
    query.bindValue(":id"_L1, player->id());

    if (!query.exec()) {
        return std::unexpected(query.lastError().text());
    }

    m_players.erase(m_players.begin() + startingRank - 1);

    Q_EMIT numberOfPlayersChanged();
    Q_EMIT numberOfRatedPlayersChanged();

    return {};
}

void Tournament::savePlayer(Player *player)
{
    QSqlQuery query(m_event->db());
    query.prepare(UPDATE_PLAYER_QUERY);
    query.bindValue(u":id"_s, player->id());
    query.bindValue(u":startingRank"_s, player->startingRank());
    query.bindValue(u":title"_s, player->title());
    query.bindValue(u":name"_s, player->name());
    query.bindValue(u":rating"_s, player->rating());
    query.bindValue(u":nationalRating"_s, player->nationalRating());
    query.bindValue(u":playerId"_s, player->playerId());
    query.bindValue(u":nationalId"_s, player->nationalId());
    query.bindValue(u":birthDate"_s, player->birthDate());
    query.bindValue(u":federation"_s, player->federation());
    query.bindValue(u":origin"_s, player->origin());
    query.bindValue(u":gender"_s, player->gender());
    query.bindValue(u":extra"_s, player->extraString());
    query.bindValue(u":tournament"_s, m_id);

    if (!query.exec()) {
        qDebug() << "save player" << *player << query.lastError();
    }

    Q_EMIT numberOfRatedPlayersChanged();
}

void Tournament::sortPlayers()
{
    std::ranges::sort(m_players, [](const std::unique_ptr<Player> &p1, const std::unique_ptr<Player> &p2) {
        if (p1->rating() == p2->rating()) {
            if (Player::titleStrengthLevel(p1->title()) == Player::titleStrengthLevel(p2->title())) {
                auto cmp = p1->name().toLower().localeAwareCompare(p2->name().toLower());
                return cmp < 0;
            }
            return Player::titleStrengthLevel(p1->title()) < Player::titleStrengthLevel(p2->title());
        }
        return p1->rating() > p2->rating();
    });

    for (int i = 0; i < static_cast<int>(m_players.size()); i++) {
        auto *player = m_players.at(i).get();
        player->setStartingRank(i + 1);
        savePlayer(player);
    }
}

int Tournament::changePlayerStartingRank(Player *player, int startingRank)
{
    Q_ASSERT(startingRank >= 1);

    const auto rank = std::min(startingRank, numberOfPlayers());

    if (rank == player->startingRank()) {
        return rank;
    }

    const auto &players = m_players;
    for (const auto &p : players) {
        if (p->startingRank() >= rank && p->startingRank() < player->startingRank()) {
            p->setStartingRank(p->startingRank() + 1);
            savePlayer(p.get());
        } else if (p->startingRank() > player->startingRank() && p->startingRank() <= rank) {
            p->setStartingRank(p->startingRank() - 1);
            savePlayer(p.get());
        }
    }

    player->setStartingRank(rank);
    savePlayer(player);

    return rank;
}

QMap<uint, Player *> Tournament::playersByStartingRank()
{
    QMap<uint, Player *> players;

    for (const auto &player : std::as_const(m_players)) {
        players[player->startingRank()] = player.get();
    }

    return players;
}

QMap<QString, Player *> Tournament::playersById()
{
    QMap<QString, Player *> players;

    for (const auto &player : std::as_const(m_players)) {
        players[player->id()] = player.get();
    }

    return players;
}

QHash<Player *, QList<Pairing *>> Tournament::pairingsByPlayer(std::optional<int> maxRound)
{
    QHash<Player *, QList<Pairing *>> result;

    const auto r = maxRound ? *maxRound : m_rounds.size();
    for (std::size_t i = 0; i < r; i++) {
        if (i >= m_rounds.size()) {
            break;
        }
        const auto round = m_rounds.at(i).get();
        const auto pairings = round->pairings();
        for (const auto &pairing : pairings) {
            result[pairing->whitePlayer()] << pairing;
            if (pairing->blackPlayer() != nullptr) {
                result[pairing->blackPlayer()] << pairing;
            }
        }
    }

    return result;
}

QList<Pairing *> Tournament::pairingsOfPlayer(Player *player)
{
    QList<Pairing *> result;

    for (int i = 0; i < m_numberOfRounds; ++i) {
        if (i >= static_cast<int>(m_rounds.size())) {
            result << nullptr;
            continue;
        }

        Pairing *p = nullptr;
        const auto &round = m_rounds.at(i);
        const auto pairings = round->pairings();
        for (const auto &pairing : pairings) {
            if (pairing->whitePlayer() == player || pairing->blackPlayer() == player) {
                p = pairing;
            }
        }
        result << p;
    }

    return result;
}

QList<Standing> Tournament::standings(const State &state)
{
    QList<Standing> standings;

    // Sort by tiebreaks
    auto sortStandings = [&standings]() {
        std::ranges::sort(standings, [](const Standing &p1, const Standing &p2) {
            for (size_t i = 0; i < p1.values().size(); i++) {
                if (p1.values().at(i) == p2.values().at(i)) {
                    continue;
                }
                return p1.values().at(i) > p2.values().at(i);
            }
            return p1.player()->startingRank() < p2.player()->startingRank();
        });
    };

    for (const auto &player : std::as_const(m_players)) {
        standings << Standing(player.get(), {});
    }

    // Calculate tiebreaks
    QList<Player *> players;
    for (const auto &tiebreak : std::as_const(m_tiebreaks)) {
        uint i = 0;
        players.clear();
        while (i < m_players.size()) {
            if (players.isEmpty()) {
                players << standings.at(i).player();
                i++;
                continue;
            }
            if (standings.at(i - 1).values() == standings.at(i).values()) {
                players << standings.at(i).player();
                i++;
            } else {
                for (int j = 0; j < players.size(); j++) {
                    /*for (const auto &p : players) {
                        qDebug() << p->name();
                    }
                    qDebug() << "Player" << j << "is" << players.at(j)->name();
                    qDebug() << "SPlayer" << j << "is" << standings.at(i - players.size() + j).first->name();
                    qDebug() << "--";*/
                    standings[i - players.size() + j].addValue(tiebreak->calculate(this, state, players, players.at(j)));
                }
                players.clear();
                players << standings.at(i).player();
                i++;
            }
        }
        for (int j = 0; j < players.size(); j++) {
            /*for (const auto &p : players) {
                qDebug() << p->name();
            }
            qDebug() << "Player" << j << "is" << players.at(j)->name();
            qDebug() << "SPlayer" << j << "is" << standings.at(i - players.size() + j).first->name();
            qDebug() << "--";*/
            standings[i - players.size() + j].addValue(tiebreak->calculate(this, state, players, players.at(j)));
        }
        sortStandings();
    }

    // Calculate ranks
    for (int i = 1; i < standings.size(); ++i) {
        auto &standing = standings[i];
        if (standing.values() == standings.at(i - 1).values()) {
            standing.setRank(standings.at(i - 1).rank());
        } else {
            standing.setRank(i + 1);
        }
    }

    return standings;
}

QList<QVariantMap> Tournament::availableTiebreaks()
{
    return {
        {
            {"id"_L1, "pts"_L1},
            {"name"_L1, i18nc("Game points", "Points")},
        },
        {
            {"id"_L1, "bh"_L1},
            {"name"_L1, i18nc("Buchholz tiebreak", "Buchholz")},
        },
        {
            {"id"_L1, "win"_L1},
            {"name"_L1, i18nc("Number of Wins tiebreak", "Number of Wins")},
        },
        {
            {"id"_L1, "won"_L1},
            {"name"_L1,
             i18nc("Number of Games Won (over the board) tiebreak. Different from the 'Number of Wins' tiebreak, which includes games won by forfeit.",
                   "Number of Games Won (over the board)")},
        },
        {
            {"id"_L1, "bpg"_L1},
            {"name"_L1, i18nc("Number of Games Played with Black (over the board) tiebreak", "Number of Games Played with Black (over the board)")},
        },
        {
            {"id"_L1, "aob"_L1},
            {"name"_L1, i18nc("Tiebreak", "Average Buchholz of Opponents")},
        },
    };
}

std::unique_ptr<Tiebreak> Tournament::tiebreak(const QString &id)
{
    if (id == "pts"_L1) {
        return std::make_unique<Points>();
    }
    if (id == "bh"_L1) {
        return std::make_unique<Buchholz>();
    }
    if (id == "win"_L1) {
        return std::make_unique<NumberOfWins>();
    }
    if (id == "won"_L1) {
        return std::make_unique<NumberOfGamesWon>();
    }
    if (id == "bpg"_L1) {
        return std::make_unique<NumberOfGamesPlayedWithBlack>();
    }
    if (id == "aob"_L1) {
        return std::make_unique<AverageBuchholzOfOpponents>();
    }
    return nullptr;
}

std::expected<std::unique_ptr<Tiebreak>, QString> Tournament::tiebreakFromTrf(const QString &code)
{
    if (code.startsWith("OTHER_"_L1, Qt::CaseSensitivity::CaseInsensitive)) {
        qWarning() << "Unsupported tiebreak" << code;
        return nullptr;
    }

    const auto options = code.split(u'/', Qt::SkipEmptyParts);

    auto tiebreak = Tournament::tiebreak(options[0].toLower());
    if (tiebreak == nullptr) {
        qWarning() << "Unsupported tiebreak" << code;
        return nullptr;
    }

    if (const auto ok = tiebreak->setTrfOptions(options.mid(1)); !ok) {
        return std::unexpected(ok.error());
    }

    return tiebreak;
}

void Tournament::setInitialColor(Tournament::InitialColor color)
{
    if (m_initialColor == color) {
        return;
    }
    m_initialColor = color;
    setOption(u"initial_color"_s, std::to_underlying(color));
}

Round *Tournament::round(int number) const
{
    Q_ASSERT(number >= 1);

    if (static_cast<size_t>(number) > m_rounds.size()) {
        return nullptr;
    }
    return m_rounds[number - 1].get();
}

std::expected<void, QString> Tournament::ensureRoundExists(int round)
{
    Q_ASSERT(round >= 1);

    if (m_rounds.size() < static_cast<size_t>(round)) {
        for (size_t i = m_rounds.size() + 1; i <= static_cast<size_t>(round); ++i) {
            auto round = std::make_unique<Round>();

            QSqlQuery query(m_event->db());
            query.prepare(ADD_ROUND_QUERY);
            query.bindValue(u":number"_s, static_cast<qulonglong>(i));
            query.bindValue(u":tournament"_s, m_id);
            query.bindValue(u":extra"_s, round->extraString());

            if (!query.exec()) {
                return std::unexpected(query.lastError().text());
            }

            round->setId(query.lastInsertId().toInt());
            round->setNumber(int(i));

            m_rounds.push_back(std::move(round));
        }
    }

    return {};
}

std::expected<void, QString> Tournament::saveRound(Round *round)
{
    const auto dateTime = round->dateTime().toUTC().toString(Qt::ISODate);

    QSqlQuery query(m_event->db());
    query.prepare(UPDATE_ROUND_QUERY);
    query.bindValue(u":id"_s, round->id());
    query.bindValue(u":datetime"_s, dateTime);
    query.bindValue(u":extra"_s, round->extraString());

    if (!query.exec()) {
        qDebug() << "save round" << query.lastError();
        return std::unexpected(query.lastError().text());
    }

    return {};
}

QMap<int, Round *> Tournament::roundsById()
{
    QMap<int, Round *> rounds;

    for (const auto &round : std::as_const(m_rounds)) {
        rounds[round->id()] = round.get();
    }

    return rounds;
}

std::expected<void, QString> Tournament::addPairing(int roundNumber, std::unique_ptr<Pairing> pairing)
{
    if (const auto ok = savePairing(pairing.get(), roundNumber); !ok) {
        return ok;
    }

    const auto round = this->round(roundNumber);
    round->addPairing(std::move(pairing));

    return {};
}

std::expected<void, QString> Tournament::savePairing(Pairing *pairing, int roundNumber)
{
    Q_ASSERT(roundNumber >= 0);

    QSqlQuery query(m_event->db());

    if (pairing->id().isEmpty()) {
        Q_ASSERT(roundNumber >= 1);

        if (const auto ok = ensureRoundExists(roundNumber); !ok) {
            return ok;
        }

        const auto round = this->round(roundNumber);

        pairing->setId(QUuid::createUuid().toString(QUuid::WithoutBraces));

        query.prepare(ADD_PAIRING_QUERY);
        query.bindValue(u":round"_s, round->id());
    } else {
        query.prepare(UPDATE_PAIRING_QUERY);
    }

    pairing->setLastModified(QDateTime::currentDateTimeUtc());

    query.bindValue(u":id"_s, pairing->id());
    query.bindValue(u":board"_s, pairing->board());
    query.bindValue(u":whitePlayer"_s, pairing->whitePlayer()->id());
    if (pairing->blackPlayer() != nullptr) {
        query.bindValue(u":blackPlayer"_s, pairing->blackPlayer()->id());
    } else {
        query.bindValue(u":blackPlayer"_s, QVariant(QMetaType::fromType<QString>()));
    }
    query.bindValue(u":whiteResult"_s, std::to_underlying(pairing->whiteResult()));
    query.bindValue(u":blackResult"_s, std::to_underlying(pairing->blackResult()));
    query.bindValue(u":lastModified"_s, pairing->lastModified().toSecsSinceEpoch());
    query.bindValue(u":extra"_s, pairing->extraString());

    if (!query.exec()) {
        qDebug() << "save pairing" << query.lastError();
        return std::unexpected(query.lastError().text());
    }

    return {};
}

std::expected<void, QString> Tournament::setResult(Pairing *pairing, Pairing::Result result)
{
    pairing->setResult(result);

    if (auto ok = savePairing(pairing); !ok) {
        return ok;
    }

    return {};
}

std::expected<void, QString> Tournament::setBye(Player *player, int round, Pairing::PartialResult result)
{
    Q_ASSERT(round > m_currentRound);

    const auto pairing = this->pairing(round, player);

    if (pairing == nullptr && result == Pairing::PartialResult::Unknown) {
        return {};
    }

    if (pairing == nullptr) {
        const qsizetype board = pairings(round).size() + 1;
        auto p = std::make_unique<Pairing>(board, player, nullptr, result, Pairing::PartialResult::Unknown);

        return addPairing(round, std::move(p));
    }

    if (result == Pairing::PartialResult::Unknown) {
        return removePairing(round, pairing);
    }

    pairing->setWhiteResult(result);

    return savePairing(pairing);
}

std::expected<void, QString> Tournament::retire(Player *player)
{
    for (int i = m_currentRound + 1; i <= m_numberOfRounds; ++i) {
        if (const auto ok = setBye(player, i, Pairing::PartialResult::ZeroBye); !ok) {
            return ok;
        }
    }

    return {};
}

QList<Player *> Tournament::voluntaryByes(int round) const
{
    Q_ASSERT(round >= 1);

    QList<Player *> result{};

    const auto pairs = pairings(round);
    for (const auto p : pairs) {
        if (Pairing::isVoluntaryBye(p->whiteResult())) {
            result << p->whitePlayer();
        }
    }

    return result;
}

Pairing *Tournament::pairing(int round, int board)
{
    Q_ASSERT(round >= 1);
    Q_ASSERT(static_cast<std::size_t>(round) <= m_rounds.size());
    Q_ASSERT(board >= 1);

    return m_rounds.at(round - 1)->pairing(board);
}

QList<Pairing *> Tournament::pairings(int round) const
{
    if (static_cast<std::size_t>(round) <= m_rounds.size()) {
        return m_rounds.at(round - 1)->pairings();
    }

    return {};
}

Pairing *Tournament::pairing(int round, Player *player) const
{
    const auto all = pairings(round);

    for (const auto &p : all) {
        if (p->whitePlayer() == player || p->blackPlayer() == player) {
            return p;
        }
    }

    return nullptr;
}

int Tournament::numberOfPlayers()
{
    return static_cast<int>(m_players.size());
}

int Tournament::numberOfRatedPlayers()
{
    return std::count_if(m_players.cbegin(), m_players.cend(), [](const auto &p) {
        return p->rating() > 0;
    });
}

std::expected<void, QString> Tournament::sortPairings(std::optional<int> round)
{
    Q_ASSERT(!round || *round != 0);

    const auto firstRound = round ? *round - 1 : 0;
    const auto lastRound = round ? *round : m_rounds.size();

    for (size_t i = firstRound; i < lastRound; i++) {
        auto &pairings = m_rounds[i]->m_pairings;

        const auto state = this->state(static_cast<int>(i));

        std::ranges::sort(pairings, [i, &state](const std::unique_ptr<Pairing> &a, const std::unique_ptr<Pairing> &b) -> bool {
            int aRank;
            if (a->blackPlayer() == nullptr) {
                aRank = 0;
            } else {
                aRank = std::min(a->whitePlayer()->startingRank(), a->blackPlayer()->startingRank());
            }

            int bRank;
            if (b->blackPlayer() == nullptr) {
                bRank = 0;
            } else {
                bRank = std::min(b->whitePlayer()->startingRank(), b->blackPlayer()->startingRank());
            }

            if (aRank == 0 && bRank == 0) {
                if (a->whiteResult() == b->whiteResult()) {
                    return a->whitePlayer()->startingRank() < b->whitePlayer()->startingRank();
                }
                return std::to_underlying(a->whiteResult()) > std::to_underlying(b->whiteResult());
            }
            if (aRank == 0) {
                return false;
            }
            if (bRank == 0) {
                return true;
            }

            double aScore;
            double aTotal;
            if (a->whitePlayer()->startingRank() < a->blackPlayer()->startingRank()) {
                aScore = state.points(a->whitePlayer());
                aTotal = aScore + state.points(a->blackPlayer());
            } else {
                aScore = state.points(a->blackPlayer());
                aTotal = aScore + state.points(a->whitePlayer());
            }
            double bScore;
            double bTotal;
            if (b->whitePlayer()->startingRank() < b->blackPlayer()->startingRank()) {
                bScore = state.points(b->whitePlayer());
                bTotal = bScore + state.points(b->blackPlayer());
            } else {
                bScore = state.points(b->blackPlayer());
                bTotal = bScore + state.points(b->whitePlayer());
            }

            if (i > 0) {
                if (aScore != bScore) {
                    return aScore > bScore;
                }

                //
                if (aTotal != bTotal) {
                    return aTotal > bTotal;
                }
            }

            return aRank < bRank;
        });

        for (size_t i = 0; i < pairings.size(); i++) {
            pairings.at(i)->setBoard(static_cast<int>(i) + 1);

            if (auto ok = savePairing(pairings[i].get()); !ok) {
                return ok;
            }
        }

        // m_rounds.at(i)->setPairings(pairings);
    }

    return {};
}

bool Tournament::isRoundFinished(int round)
{
    qDebug() << "is round finished" << round;
    Q_ASSERT(round >= 1);

    QList<Pairing *> pairings;

    if (static_cast<std::size_t>(round) <= m_rounds.size()) {
        pairings = m_rounds.at(round - 1)->pairings();
    } else {
        return false; // TODO: ok?
    }

    auto finished = std::count_if(pairings.cbegin(), pairings.cend(), [](Pairing *p) {
        return Pairing::isBye(p->whiteResult()) || (p->whiteResult() != Pairing::PartialResult::Unknown && p->blackResult() != Pairing::PartialResult::Unknown);
    });

    return finished == pairings.size();
}

bool Tournament::isRoundFullyPaired(int round)
{
    Q_ASSERT(round >= 1);
    Q_ASSERT(static_cast<std::size_t>(round) <= m_rounds.size());

    const auto pairings = m_rounds.at(round - 1)->pairings();

    QSet<Player *> players;

    for (const auto &pairing : pairings) {
        players.insert(pairing->whitePlayer());
        if (pairing->blackPlayer() != nullptr) {
            players.insert(pairing->blackPlayer());
        }
    }

    return static_cast<qsizetype>(m_players.size()) == players.size();
}

QCoro::Task<std::expected<QList<std::pair<uint, uint>>, QString>> Tournament::calculatePairings(int round)
{
    auto engine = std::make_unique<PairingEngine>();
    const auto pairings = co_await engine->pair(round, this);

    if (!pairings.has_value()) {
        co_return std::unexpected(pairings.error());
    }

    co_return pairings;
}

QCoro::Task<std::expected<bool, QString>> Tournament::pairNextRound()
{
    const auto roundToPair = m_currentRound + 1;
    const auto pairings = co_await calculatePairings(roundToPair);

    if (!pairings.has_value()) {
        co_return std::unexpected(i18n("An error ocurred while pairing the round: %1", pairings.error()));
    }

    const auto players = playersByStartingRank();

    uint board = 1;
    for (const auto &pairing : *pairings) {
        const auto &whitePlayer = players.value(pairing.first);
        Player *blackPlayer = nullptr;
        Pairing::PartialResult whiteResult = Pairing::PartialResult::PairingBye;

        if (pairing.second != 0) {
            blackPlayer = players.value(pairing.second);
            whiteResult = Pairing::PartialResult::Unknown;
        }

        auto p = std::make_unique<Pairing>(board, whitePlayer, blackPlayer, whiteResult, Pairing::PartialResult::Unknown);
        if (auto ok = addPairing(roundToPair, std::move(p)); !ok) {
            co_return std::unexpected(ok.error());
        }

        board++;
    }

    if (auto ok = sortPairings(roundToPair); !ok) {
        co_return std::unexpected(ok.error());
    }

    setCurrentRound(roundToPair);

    co_return true;
}

std::expected<void, QString> Tournament::removePairing(int round, Pairing *pairing)
{
    Q_ASSERT(round >= 1);
    Q_ASSERT(pairing != nullptr);
    Q_ASSERT(!pairing->id().isEmpty());

    QSqlQuery query(m_event->db());
    query.prepare(DELETE_PAIRING_QUERY);
    query.bindValue(":id"_L1, pairing->id());

    if (!query.exec()) {
        return std::unexpected(query.lastError().text());
    }

    m_rounds.at(round - 1)->removePairings([&pairing](Pairing *p) {
        return p->id() == pairing->id();
    });

    return {};
}

std::expected<void, QString> Tournament::removePairings(int round, bool keepByes)
{
    Q_ASSERT(round >= 1);
    Q_ASSERT(round <= m_numberOfRounds);

    for (size_t i = round; i <= m_rounds.size(); i++) {
        QSqlQuery query(m_event->db());
        if (keepByes) {
            query.prepare(DELETE_PAIRINGS_KEEP_BYES_QUERY);
        } else {
            query.prepare(DELETE_PAIRINGS_QUERY);
        }
        query.bindValue(u":round"_s, m_rounds.at(i - 1)->id());

        if (!query.exec()) {
            qDebug() << "remove pairings" << query.lastError();
            return std::unexpected(query.lastError().text());
        }

        m_rounds.at(i - 1)->removePairings([keepByes](Pairing *pairing) {
            return !keepByes || !Pairing::isVoluntaryBye(pairing->whiteResult());
        });
    }

    setCurrentRound(round - 1);

    return {};
}

std::expected<void, QString> Tournament::deletePairings(Player *player)
{
    Q_ASSERT(m_currentRound == 0);
    Q_ASSERT(player != nullptr);
    Q_ASSERT(!player->id().isEmpty());

    QSqlQuery query(m_event->db());
    query.prepare(DELETE_PAIRINGS_OF_PLAYER_QUERY);
    query.bindValue(":id"_L1, player->id());

    if (!query.exec()) {
        return std::unexpected(query.lastError().text());
    }

    for (auto &round : m_rounds) {
        round->removePairings([&player](Pairing *pairing) {
            return pairing->whitePlayer() == player || pairing->blackPlayer() == player;
        });
    }

    return {};
}

Tournament::InitialColor Tournament::initialColor()
{
    return m_initialColor;
}

State Tournament::state(std::optional<int> maxRound)
{
    return State{this, maxRound};
}

void Tournament::saveTiebreaks()
{
    QJsonArray values;

    for (const auto &tiebreak : tiebreaks()) {
        values << tiebreak->toJson();
    }

    const auto doc = QJsonDocument{QJsonObject{{"tiebreaks"_L1, values}}};
    const auto text = doc.toJson(QJsonDocument::Compact);

    setOption("tiebreaks"_L1, text);

    Q_EMIT tiebreaksChanged();
}

QVariant Tournament::option(const QString &name)
{
    return m_options.value(name);
}

void Tournament::setOption(const QString &name, const QVariant &value)
{
    QSqlQuery query(m_event->db());
    query.prepare(UPDATE_OPTION_QUERY);
    query.bindValue(u":tournament"_s, m_id);
    query.bindValue(u":name"_s, name);
    query.bindValue(u":value"_s, value);

    if (!query.exec()) {
        qDebug() << "set option" << name << value << query.lastError();
        return;
    }

    m_options[name] = value;
}

QJsonObject Tournament::toJson() const
{
    QJsonObject tournament;
    tournament["id"_L1] = m_id;
    tournament["name"_L1] = m_name;
    tournament["slug"_L1] = Utils::normalize(m_name.toLower()).replace(" "_L1, "-"_L1);
    tournament["federation"_L1] = m_federation;
    tournament["city"_L1] = m_city;
    tournament["number_rounds"_L1] = m_numberOfRounds;

    QJsonArray players;
    for (const auto &player : std::as_const(m_players)) {
        players << player->toJson();
    }
    tournament["players"_L1] = players;

    QJsonArray rounds;
    for (const auto &round : std::as_const(m_rounds)) {
        rounds << round->toJson();
    }
    tournament["rounds"_L1] = rounds;

    return tournament;
}

void Tournament::read(const QJsonObject &json)
{
    if (auto v = json[QStringLiteral("tournament")]; v.isObject()) {
        auto tournament = v.toObject();

        if (const auto v = tournament[QStringLiteral("id")]; v.isString()) {
            m_id = v.toString();
        }
        if (const auto v = tournament[QStringLiteral("name")]; v.isString()) {
            m_name = v.toString();
        }
        if (const auto v = tournament[QStringLiteral("city")]; v.isString()) {
            m_city = v.toString();
        }
        if (const auto v = tournament[QStringLiteral("federation")]; v.isString()) {
            m_federation = v.toString();
        }
        if (const auto v = tournament[QStringLiteral("number_of_rounds")]; v.isDouble()) {
            m_numberOfRounds = v.toInt();
        }
    }

    if (auto v = json[QStringLiteral("players")]; v.isArray()) {
        auto players = v.toArray();
        m_players.clear();
        m_players.reserve(players.size());
        for (const auto &player : std::as_const(players)) {
            m_players.push_back(Player::fromJson(player.toObject()));
        }
    }
}

std::expected<void, QString> Tournament::readTrf(QTextStream trf)
{
    TrfReader reader{this};
    return reader.read(&trf);
}

QString Tournament::toTrf(Trf::Options options, std::optional<int> maxRound)
{
    QString result;
    QTextStream stream(&result);

    TrfWriter writer(this, options, maxRound);
    writer.write(stream);

    return result;
}

std::expected<void, QString> Tournament::loadTrf(const QString &fileName)
{
    QFile file(fileName);

    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        return std::unexpected(i18n("Couldn't open file"));
    }

    return readTrf(QTextStream(&file));
}

bool Tournament::exportTrf(const QString &fileName)
{
    QFile file(fileName);

    if (!file.open(QIODevice::WriteOnly)) {
        qWarning() << "Couldn't export tournament report" << fileName;
        return false;
    }

    const auto trf = toTrf();
    file.write(trf.toUtf8());

    return true;
}

std::expected<void, QString> Tournament::createNewTournament()
{
    Q_ASSERT(m_id.isEmpty());

    const auto newId = QUuid::createUuid().toString(QUuid::WithoutBraces);

    QSqlQuery query(m_event->db());
    query.prepare(ADD_TOURNAMENT_QUERY);
    query.bindValue(u":id"_s, newId);

    if (!query.exec()) {
        qDebug() << "create tournament" << query.lastError();
        return std::unexpected(query.lastError().text());
    }

    setId(newId);

    return {};
}

std::expected<void, QString> Tournament::loadTournament(const QString &id)
{
    Q_ASSERT(m_id.isEmpty());

    setId(id);

    if (const auto ok = loadOptions(); !ok) {
        return ok;
    }
    if (const auto ok = loadPlayers(); !ok) {
        return ok;
    }
    if (const auto ok = loadRounds(); !ok) {
        return ok;
    }
    if (const auto ok = loadPairings(); !ok) {
        return ok;
    }
    if (const auto ok = loadTiebreaks(); !ok) {
        return ok;
    }
    if (const auto ok = loadArbiters(); !ok) {
        return ok;
    }

    return {};
}

std::expected<void, QString> Tournament::loadOptions()
{
    QSqlQuery query(m_event->db());
    query.prepare(GET_OPTIONS_QUERY);
    query.bindValue(u":tournament"_s, m_id);

    if (!query.exec()) {
        qDebug() << "Error loading options" << query.lastError();
        return std::unexpected(query.lastError().text());
    }

    const int nameNo = query.record().indexOf("name");
    const int valueNo = query.record().indexOf("value");

    while (query.next()) {
        const auto name = query.value(nameNo).toString();
        const auto value = query.value(valueNo);
        m_options[name] = value;
    }

    setName(option(u"name"_s).toString());
    setCity(option(u"city"_s).toString());
    setFederation(option(u"federation"_s).toString());
    setNumberOfRounds(option(u"number_of_rounds"_s).toInt());
    setCurrentRound(option(u"current_round"_s).toInt());
    setInitialColor(Tournament::InitialColor(option(u"initial_color"_s).toInt()));

    return {};
}

std::expected<void, QString> Tournament::loadPlayers()
{
    m_players.clear();

    QSqlQuery query(m_event->db());
    query.prepare(GET_PLAYERS_QUERY);
    query.bindValue(u":tournament"_s, m_id);

    if (!query.exec()) {
        qDebug() << "Error loading players" << query.lastError();
        return std::unexpected(query.lastError().text());
    }

    const int idNo = query.record().indexOf("id");
    const int stRankNo = query.record().indexOf("startingRank");
    const int titleNo = query.record().indexOf("title");
    const int nameNo = query.record().indexOf("name");
    const int ratingNo = query.record().indexOf("rating");
    const int nationalRatingNo = query.record().indexOf("nationalRating");
    const int playerIdNo = query.record().indexOf("playerId");
    const int nationalIdNo = query.record().indexOf("nationalId");
    const int birthDateNo = query.record().indexOf("birthDate");
    const int federationNo = query.record().indexOf("federation");
    const int originNo = query.record().indexOf("origin");
    const int genderNo = query.record().indexOf("gender");
    const int extraNo = query.record().indexOf("extra");

    while (query.next()) {
        auto player = std::make_unique<Player>(query.value(stRankNo).toInt(),
                                               query.value(titleNo).toString(),
                                               query.value(nameNo).toString(),
                                               query.value(ratingNo).toInt(),
                                               query.value(nationalRatingNo).toInt(),
                                               query.value(playerIdNo).toString(),
                                               query.value(birthDateNo).toString(),
                                               query.value(federationNo).toString(),
                                               query.value(originNo).toString(),
                                               query.value(genderNo).toString());
        player->setId(query.value(idNo).toString());
        player->setNationalId(query.value(nationalIdNo).toString());
        player->setExtra(query.value(extraNo).toByteArray());
        m_players.push_back(std::move(player));
    }

    return {};
}

std::expected<void, QString> Tournament::loadRounds()
{
    QSqlQuery query(m_event->db());
    query.prepare(GET_ROUNDS_QUERY);
    query.bindValue(u":tournament"_s, m_id);

    if (!query.exec()) {
        qDebug() << "Error loading rounds" << query.lastError();
        return std::unexpected(query.lastError().text());
    }

    const int idNo = query.record().indexOf("id");
    const int numberNo = query.record().indexOf("number");
    const int dateTimeNo = query.record().indexOf("datetime");
    const int extraNo = query.record().indexOf("extra");

    while (query.next()) {
        auto round = std::make_unique<Round>();
        round->setId(query.value(idNo).toInt());
        round->setNumber(query.value(numberNo).toInt());
        round->setDateTime(query.value(dateTimeNo).toDateTime());
        round->setExtra(query.value(extraNo).toByteArray());
        m_rounds.push_back(std::move(round));
    }

    std::ranges::sort(m_rounds, [](const std::unique_ptr<Round> &a, const std::unique_ptr<Round> &b) {
        return a->number() < b->number();
    });

    return {};
}

std::expected<void, QString> Tournament::loadPairings()
{
    const auto players = playersById();
    const auto rounds = roundsById();

    QSqlQuery query(m_event->db());
    query.prepare(GET_PAIRINGS_QUERY);
    query.bindValue(u":tournament"_s, m_id);

    if (!query.exec()) {
        qDebug() << "Error loading pairings" << query.lastError();
        return std::unexpected(query.lastError().text());
    }

    const int idNo = query.record().indexOf("id");
    const int boardNo = query.record().indexOf("board");
    const int whitePlayerNo = query.record().indexOf("whitePlayer");
    const int blackPlayerNo = query.record().indexOf("blackPlayer");
    const int whiteResultNo = query.record().indexOf("whiteResult");
    const int blackResultNo = query.record().indexOf("blackResult");
    const int roundNo = query.record().indexOf("round");
    const int lastModifiedNo = query.record().indexOf("lastModified");
    const int extraNo = query.record().indexOf("extra");

    while (query.next()) {
        const auto round = query.value(roundNo).toInt();
        Q_ASSERT(rounds.contains(round));

        auto pairing = std::make_unique<Pairing>(query.value(boardNo).toInt(),
                                                 players.value(query.value(whitePlayerNo).toString()),
                                                 players.value(query.value(blackPlayerNo).toString()),
                                                 Pairing::PartialResult(query.value(whiteResultNo).toInt()),
                                                 Pairing::PartialResult(query.value(blackResultNo).toInt()));
        pairing->setId(query.value(idNo).toString());
        pairing->setLastModified(QDateTime::fromSecsSinceEpoch(query.value(lastModifiedNo).toLongLong()));
        pairing->setExtra(query.value(extraNo).toByteArray());

        rounds.value(round)->addPairing(std::move(pairing));
    }

    return {};
}

std::expected<void, QString> Tournament::loadTiebreaks()
{
    const auto json = QJsonDocument::fromJson(option("tiebreaks"_L1).toByteArray());

    if (const auto tbs = json["tiebreaks"_L1]; tbs.isArray()) {
        m_tiebreaks.clear();

        const auto values = tbs.toArray();
        for (const auto value : values) {
            if (!value.isObject()) {
                continue;
            }

            const auto options = value.toObject();
            const auto id = options["id"_L1].toString();

            auto tiebreak = Tournament::tiebreak(id);
            if (tiebreak == nullptr) {
                tiebreak = std::make_unique<DummyTiebreak>();
            }

            tiebreak->setOptions(options.toVariantMap());

            m_tiebreaks.push_back(std::move(tiebreak));
        }
    }

    return {};
}

std::expected<void, QString> Tournament::loadArbiters()
{
    const auto json = QJsonDocument::fromJson(option("arbiters"_L1).toByteArray());

    if (const auto arbiters = json["arbiters"_L1]; arbiters.isArray()) {
        m_arbiters.clear();

        const auto values = arbiters.toArray();
        for (const auto value : values) {
            if (!value.isObject()) {
                continue;
            }

            auto arbiter = Arbiter::fromJson(value.toObject());

            m_arbiters.push_back(std::move(arbiter));
        }
    }

    return {};
}

#include "moc_tournament.cpp"
