#ifndef KOTH_REMOTE_REVERSI_BOT_CLIENT_HPP
#define KOTH_REMOTE_REVERSI_BOT_CLIENT_HPP

#include <array>
#include <cerrno>
#include <chrono>
#include <csignal>
#include <cstddef>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <limits>
#include <netdb.h>
#include <netinet/tcp.h>
#include <stdexcept>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <thread>
#include <unistd.h>
#include <utility>
#include <vector>

namespace koth {

constexpr int kBoardSize = 8;
constexpr std::size_t kMaxLineBytes = 4096;
constexpr const char* kProtocolVersion = "4";

using Board = std::array<std::string, kBoardSize>;

struct Move {
    int row = -1;
    int col = -1;

    static Move resign() { return Move{-1, -1}; }
    bool is_resignation() const { return row == -1 && col == -1; }
};

inline bool operator==(const Move& lhs, const Move& rhs) {
    return lhs.row == rhs.row && lhs.col == rhs.col;
}

inline std::vector<std::string> split_words(const std::string& line) {
    std::vector<std::string> words;
    std::size_t position = 0;
    while (position < line.size()) {
        while (position < line.size() && line[position] == ' ') ++position;
        if (position == line.size()) break;
        const std::size_t end = line.find(' ', position);
        if (end == std::string::npos) {
            words.push_back(line.substr(position));
            break;
        }
        words.push_back(line.substr(position, end - position));
        position = end + 1;
    }
    return words;
}

inline int parse_int(const std::string& token, const char* field_name) {
    std::size_t consumed = 0;
    long value = 0;
    try {
        value = std::stol(token, &consumed, 10);
    } catch (const std::exception&) {
        throw std::runtime_error(std::string("invalid integer for ") + field_name + ": " + token);
    }
    if (consumed != token.size() ||
        value < std::numeric_limits<int>::min() ||
        value > std::numeric_limits<int>::max()) {
        throw std::runtime_error(std::string("invalid integer for ") + field_name + ": " + token);
    }
    return static_cast<int>(value);
}

inline bool valid_bot_name(const std::string& name) {
    if (name.empty() || name.size() > 32) return false;
    for (unsigned char ch : name) {
        const bool allowed = (ch >= 'A' && ch <= 'Z') ||
                             (ch >= 'a' && ch <= 'z') ||
                             (ch >= '0' && ch <= '9') ||
                             ch == '_' || ch == '.' || ch == '-';
        if (!allowed) return false;
    }
    return true;
}

inline char opponent(char color) {
    if (color == 'B') return 'W';
    if (color == 'W') return 'B';
    throw std::runtime_error("invalid color");
}

inline Board initial_board() {
    Board board{};
    for (std::string& row : board) row.assign(kBoardSize, '.');
    board[3][3] = 'W';
    board[3][4] = 'B';
    board[4][3] = 'B';
    board[4][4] = 'W';
    return board;
}

inline Board parse_board(const std::string& encoded) {
    if (encoded.size() != static_cast<std::size_t>(kBoardSize * kBoardSize)) {
        throw std::runtime_error("SYNC board is not 64 cells long");
    }
    Board board{};
    for (int row = 0; row < kBoardSize; ++row) {
        board[static_cast<std::size_t>(row)] =
            encoded.substr(static_cast<std::size_t>(row * kBoardSize), kBoardSize);
        for (char cell : board[static_cast<std::size_t>(row)]) {
            if (cell != '.' && cell != 'B' && cell != 'W') {
                throw std::runtime_error("SYNC board contains an invalid cell");
            }
        }
    }
    return board;
}

inline bool inside(int row, int col) {
    return row >= 0 && row < kBoardSize && col >= 0 && col < kBoardSize;
}

inline std::vector<Move> flips_for_move(
    const Board& board, char color, int row, int col) {
    if (!inside(row, col) || board[static_cast<std::size_t>(row)][static_cast<std::size_t>(col)] != '.') {
        return {};
    }
    static constexpr int directions[8][2] = {
        {-1, -1}, {-1, 0}, {-1, 1}, {0, -1},
        {0, 1}, {1, -1}, {1, 0}, {1, 1},
    };
    const char other = opponent(color);
    std::vector<Move> result;
    for (const auto& direction : directions) {
        int r = row + direction[0];
        int c = col + direction[1];
        std::vector<Move> line;
        while (inside(r, c) &&
               board[static_cast<std::size_t>(r)][static_cast<std::size_t>(c)] == other) {
            line.push_back(Move{r, c});
            r += direction[0];
            c += direction[1];
        }
        if (!line.empty() && inside(r, c) &&
            board[static_cast<std::size_t>(r)][static_cast<std::size_t>(c)] == color) {
            result.insert(result.end(), line.begin(), line.end());
        }
    }
    return result;
}

inline std::vector<Move> legal_moves(const Board& board, char color) {
    std::vector<Move> result;
    for (int row = 0; row < kBoardSize; ++row) {
        for (int col = 0; col < kBoardSize; ++col) {
            if (!flips_for_move(board, color, row, col).empty()) {
                result.push_back(Move{row, col});
            }
        }
    }
    return result;
}

inline bool apply_move(Board& board, char color, const Move& move) {
    const std::vector<Move> flips = flips_for_move(board, color, move.row, move.col);
    if (flips.empty()) return false;
    board[static_cast<std::size_t>(move.row)][static_cast<std::size_t>(move.col)] = color;
    for (const Move& flip : flips) {
        board[static_cast<std::size_t>(flip.row)][static_cast<std::size_t>(flip.col)] = color;
    }
    return true;
}

class TcpConnection {
public:
    TcpConnection(const std::string& host, const std::string& port) {
        addrinfo hints{};
        hints.ai_family = AF_UNSPEC;
        hints.ai_socktype = SOCK_STREAM;
        hints.ai_protocol = IPPROTO_TCP;

        addrinfo* addresses = nullptr;
        const int lookup_result = ::getaddrinfo(host.c_str(), port.c_str(), &hints, &addresses);
        if (lookup_result != 0) {
            throw std::runtime_error(std::string("getaddrinfo failed: ") + ::gai_strerror(lookup_result));
        }
        for (addrinfo* address = addresses; address != nullptr; address = address->ai_next) {
            const int candidate = ::socket(address->ai_family, address->ai_socktype, address->ai_protocol);
            if (candidate < 0) continue;
            int enabled = 1;
            (void)::setsockopt(candidate, SOL_SOCKET, SO_KEEPALIVE, &enabled, sizeof(enabled));
            (void)::setsockopt(candidate, IPPROTO_TCP, TCP_NODELAY, &enabled, sizeof(enabled));
            if (::connect(candidate, address->ai_addr, address->ai_addrlen) == 0) {
                socket_fd_ = candidate;
                break;
            }
            ::close(candidate);
        }
        ::freeaddrinfo(addresses);
        if (socket_fd_ < 0) {
            throw std::runtime_error(
                "could not connect to " + host + ":" + port + ": " + std::strerror(errno));
        }
    }

    TcpConnection(const TcpConnection&) = delete;
    TcpConnection& operator=(const TcpConnection&) = delete;

    ~TcpConnection() {
        if (socket_fd_ >= 0) ::close(socket_fd_);
    }

    void send_line(const std::string& line) {
        std::string payload = line;
        payload.push_back('\n');
        std::size_t sent = 0;
        while (sent < payload.size()) {
            const ssize_t result = ::send(
                socket_fd_, payload.data() + sent, payload.size() - sent, 0);
            if (result < 0) {
                if (errno == EINTR) continue;
                throw std::runtime_error(std::string("send failed: ") + std::strerror(errno));
            }
            if (result == 0) throw std::runtime_error("socket closed while sending");
            sent += static_cast<std::size_t>(result);
        }
    }

    bool read_line(std::string& line) {
        while (true) {
            const std::size_t newline = receive_buffer_.find('\n');
            if (newline != std::string::npos) {
                line = receive_buffer_.substr(0, newline);
                receive_buffer_.erase(0, newline + 1);
                if (!line.empty() && line.back() == '\r') line.pop_back();
                return true;
            }
            char chunk[1024];
            const ssize_t received = ::recv(socket_fd_, chunk, sizeof(chunk), 0);
            if (received < 0) {
                if (errno == EINTR) continue;
                throw std::runtime_error(std::string("recv failed: ") + std::strerror(errno));
            }
            if (received == 0) return false;
            receive_buffer_.append(chunk, static_cast<std::size_t>(received));
            if (receive_buffer_.size() > kMaxLineBytes) {
                throw std::runtime_error("server sent an oversized line");
            }
        }
    }

private:
    int socket_fd_ = -1;
    std::string receive_buffer_;
};

struct BotConfig {
    std::string name;
    std::string token;
    std::string host;
    std::string port;
};

inline std::string getenv_or(const char* name, const std::string& fallback) {
    const char* value = std::getenv(name);
    return value != nullptr && value[0] != '\0' ? std::string(value) : fallback;
}

inline BotConfig load_config(int argc, char** argv) {
    BotConfig config{
        getenv_or("KOTH_BOT_NAME", ""),
        getenv_or("KOTH_BOT_TOKEN", ""),
        getenv_or("KOTH_HOST", "127.0.0.1"),
        getenv_or("KOTH_PORT", "9000"),
    };
    if (argc == 4) {
        config.name = argv[1];
        config.host = argv[2];
        config.port = argv[3];
    } else if (argc == 5) {
        config.name = argv[1];
        config.token = argv[2];
        config.host = argv[3];
        config.port = argv[4];
    } else if (argc != 1) {
        throw std::runtime_error(
            std::string("Usage:\n  ") + argv[0] +
            " BOT_NAME HOST PORT                 # token from KOTH_BOT_TOKEN\n  " +
            argv[0] + " BOT_NAME BOT_TOKEN HOST PORT\n");
    }
    if (!valid_bot_name(config.name)) {
        throw std::runtime_error("BOT_NAME must match [A-Za-z0-9_.-]{1,32}");
    }
    if (config.token.rfind("koth_bot_", 0) != 0) {
        throw std::runtime_error("KOTH_BOT_TOKEN is required");
    }
    const int numeric_port = parse_int(config.port, "port");
    if (numeric_port < 1 || numeric_port > 65535) {
        throw std::runtime_error("PORT must be between 1 and 65535");
    }
    return config;
}

class FatalArenaError : public std::runtime_error {
public:
    using std::runtime_error::runtime_error;
};

// Replace only your choose_move function. The client maintains the board and
// generates legal moves locally; the server sends only color, opponent moves,
// and the rare GO/SYNC recovery messages.
template <typename Strategy>
void run_one_connection(const BotConfig& config, Strategy choose_move) {
    TcpConnection connection(config.host, config.port);
    connection.send_line(
        std::string("HELLO ") + kProtocolVersion + " " + config.name + " " + config.token);
    std::cerr << "Connected " << config.name << " to "
              << config.host << ':' << config.port << '\n';

    Board board = initial_board();
    char my_color = '?';
    bool in_match = false;

    const auto play_turn = [&]() {
        const std::vector<Move> moves = legal_moves(board, my_color);
        if (moves.empty()) {
            throw std::runtime_error("server requested a move when none is legal");
        }
        const Move selected = choose_move(board, my_color, moves);
        if (selected.is_resignation()) {
            connection.send_line("R");
            return;
        }
        // Apply valid moves locally before sending. An intentionally invalid
        // coordinate is still sent; the authoritative server will reject it.
        (void)apply_move(board, my_color, selected);
        connection.send_line(
            std::to_string(selected.row) + " " + std::to_string(selected.col));
    };

    std::string line;
    while (connection.read_line(line)) {
        if (line.empty()) continue;
        const std::vector<std::string> words = split_words(line);
        if (words.empty()) continue;

        if (words[0] == "WELCOME") {
            if (words.size() != 5) throw std::runtime_error("malformed WELCOME");
            std::cerr << "Accepted; rating=" << words[3] << " RD=" << words[4] << '\n';
        } else if (words[0] == "START") {
            if (words.size() != 2 || words[1].size() != 1 ||
                (words[1][0] != 'B' && words[1][0] != 'W')) {
                throw std::runtime_error("malformed START");
            }
            board = initial_board();
            my_color = words[1][0];
            in_match = true;
            if (my_color == 'B') play_turn();
        } else if (words[0] == "M") {
            if (!in_match || words.size() != 3) {
                throw std::runtime_error("malformed opponent move");
            }
            const Move move{parse_int(words[1], "row"), parse_int(words[2], "column")};
            if (!apply_move(board, opponent(my_color), move)) {
                throw std::runtime_error("server sent an illegal opponent move");
            }
            if (!legal_moves(board, my_color).empty()) play_turn();
        } else if (words[0] == "GO") {
            if (!in_match || words.size() != 1) throw std::runtime_error("malformed GO");
            play_turn();
        } else if (words[0] == "SYNC") {
            if (words.size() != 3 || words[1].size() != 1 ||
                (words[1][0] != 'B' && words[1][0] != 'W')) {
                throw std::runtime_error("malformed SYNC");
            }
            my_color = words[1][0];
            board = parse_board(words[2]);
            in_match = true;
            play_turn();
        } else if (words[0] == "END") {
            if (words.size() < 7) throw std::runtime_error("malformed END");
            std::cerr << "Match ended: " << words[1]
                      << " discs=" << words[2] << '-' << words[3]
                      << " rating=" << words[4]
                      << " delta=" << words[5]
                      << " reason=" << words[6] << '\n';
            in_match = false;
            my_color = '?';
        } else if (words[0] == "ERROR") {
            if (line.find("invalid_token") != std::string::npos ||
                line.find("token_revoked") != std::string::npos ||
                line.find("name_already_registered") != std::string::npos ||
                line.find("invalid_bot_name") != std::string::npos) {
                throw FatalArenaError("arena rejected credentials: " + line);
            }
            throw std::runtime_error("arena error: " + line);
        }
    }
    throw std::runtime_error("arena closed the connection");
}

template <typename Strategy>
int run_bot(int argc, char** argv, Strategy choose_move) {
    std::signal(SIGPIPE, SIG_IGN);
    try {
        const BotConfig config = load_config(argc, argv);
        int retry_seconds = 1;
        while (true) {
            try {
                run_one_connection(config, choose_move);
            } catch (const FatalArenaError&) {
                throw;
            } catch (const std::exception& error) {
                std::cerr << "Disconnected: " << error.what()
                          << "; retrying in " << retry_seconds << "s\n";
                std::this_thread::sleep_for(std::chrono::seconds(retry_seconds));
                if (retry_seconds < 10) ++retry_seconds;
            }
        }
    } catch (const std::exception& error) {
        std::cerr << "Fatal: " << error.what() << '\n';
        return 1;
    }
}

}  // namespace koth

#endif  // KOTH_REMOTE_REVERSI_BOT_CLIENT_HPP
