#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include "chess.h"
#include "engine.h"
#include "gui.h"
#include "mistakes.h"

typedef struct
{
    Position    pos;
    Engine      engine;
    Gui         gui;
    MistakeList mistakes;
    bool        replay_mode; // allow revisiting mistakes
    size_t      replay_index;
} App;

static void append_uci(char *line, size_t sz, const char *mv)
{
    if (line[0])
        strncat(line, " ", sz - 1);
    strncat(line, mv, sz - 1);
}

static void position_push_move_text(Position *pos, const Move *m, char *line, size_t lsz)
{
    (void)pos;
    char u[8];
    move_to_uci(m, u);
    append_uci(line, lsz, u);
}

static void collect_all_legal_uci(const Position *pos, char list[][8], size_t *n, size_t max)
{
    Move   mv[MAX_MOVES];
    size_t k = chess_generate_legal_moves(pos, mv, MAX_MOVES);
    *n       = 0;
    for (size_t i = 0; i < k && *n < max; i++)
    {
        char u[8];
        move_to_uci(&mv[i], u);
        strncpy(list[(*n)++], u, 8);
    }
}

static bool uci_in_list(const char *u, char list[][8], size_t n)
{
    for (size_t i = 0; i < n; i++)
    {
        if (strncmp(u, list[i], 8) == 0)
            return true;
    }
    return false;
}

static const char *mistake_file_path(void) { return "mistakes.txt"; }

int main(void)
{
    srand((unsigned)time(NULL));

    App app;
    memset(&app, 0, sizeof(app));
    mistakes_init(&app.mistakes);
    mistakes_load(&app.mistakes, mistake_file_path());

    // Avoid SIGPIPE crashes when engine pipe closes
    (void)signal(SIGPIPE, SIG_IGN);

    if (!gui_init(&app.gui, 720, 760, "Opening Learner"))
    {
        (void)fprintf(stderr, "GUI init failed.\n");
        return 1;
    }

    if (!engine_start(&app.engine))
    {
        (void)fprintf(stderr,
                      "Error: Neither stockfish nor asmfish found locally. Please install one.\n");
        gui_destroy(&app.gui);
        return 1;
    }

    // Initialize position
    chess_init_start(&app.pos);

    // Randomly pick side
    bool player_is_white = rand() % 2 == 0;
    gui_set_flipped(&app.gui, !player_is_white);

    // gameplay state
    char         status[128]   = "";
    GuiSelection sel           = {.from_sq = -1, .to_sq = -1, .promo = 0, .clicked = false};
    char         line_uci[512] = ""; // history of moves uci

    // If white, player moves first
    bool awaiting_player         = player_is_white;
    char expected_player_move[8] = ""; // best move suggested by engine for player
    bool quit                    = false;

    while (!quit)
    {
        // Show board
        gui_draw(&app.gui, app.pos.board, &sel, status);

        // Engine to act when it's engine turn
        if (!awaiting_player)
        {
            // 2. Ask engine for proposed responses (5)
            EngineMove props[5];
            size_t     n = engine_get_top_moves(&app.engine, &app.pos, props, 5);
            // 3. Sort is done in engine; also collect legal ones not proposed
            char   legal[256][8];
            size_t lcnt = 0;
            collect_all_legal_uci(&app.pos, legal, &lcnt, 256);
            // 4. pick response with decreasing probability
            // add non-proposed at the end with minimal priority
            char   pool[300][8];
            int    weights[300];
            size_t pcnt = 0;
            for (size_t i = 0; i < n; i++)
            {
                strncpy(pool[pcnt], props[i].uci, 8);
                weights[pcnt++] = (int)(n - i);
            }
            for (size_t i = 0; i < lcnt; i++)
            {
                if (!uci_in_list(legal[i], pool, pcnt))
                {
                    memcpy(pool[pcnt], legal[i], 8);
                    pool[pcnt][7]   = '\0';
                    weights[pcnt++] = 1;
                }
            }
            // weighted pick
            int wsum = 0;
            for (size_t i = 0; i < pcnt; i++)
                wsum += weights[i];
            int    r    = (wsum > 0) ? (rand() % wsum) : 0;
            size_t pick = 0;
            for (size_t i = 0; i < pcnt; i++)
            {
                if (r < weights[i])
                {
                    pick = i;
                    break;
                }
                r -= weights[i];
            }
            // 5. play response
            Move m;
            if (!parse_uci_move(pool[pick], &app.pos, &m))
            { // fallback: best
                if (!(n > 0 && parse_uci_move(props[0].uci, &app.pos, &m)))
                {
                    (void)snprintf(status, sizeof(status), "No engine move");
                    quit = true;
                    continue;
                }
            }
            chess_make_move(&app.pos, &m);
            position_push_move_text(&app.pos, &m, line_uci, sizeof(line_uci));
            awaiting_player = true;
            // 6. Ask engine for optimal response from player
            engine_get_best_move(&app.engine, &app.pos, expected_player_move);
            (void)snprintf(status, sizeof(status), "Your turn");
            continue;
        }

        // Player move input
        int  key     = 0;
        bool updated = gui_poll_move(&app.gui, &sel, &quit, &key);
        if (quit)
            break;
        if (key == 'm' || key == 'M')
        {
            // enter simple replay: load a mistake line and restart from it to the position where
            // best move is required
            if (app.mistakes.count > 0)
            {
                if (app.replay_index >= app.mistakes.count)
                    app.replay_index = 0;
                Mistake *mk = &app.mistakes.items[app.replay_index++];
                // reset and play the line moves to reach the mistake position
                chess_init_start(&app.pos);
                char tmp[512];
                (void)snprintf(tmp, sizeof(tmp), "%s", mk->line);
                char *tok = strtok(tmp, " ");
                while (tok)
                {
                    Move m;
                    if (parse_uci_move(tok, &app.pos, &m))
                        chess_make_move(&app.pos, &m);
                    tok = strtok(NULL, " ");
                }
                (void)snprintf(status, sizeof(status), "Practice: best is %s", mk->best_move);
                strncpy(expected_player_move, mk->best_move, sizeof(expected_player_move));
                awaiting_player = true;
                gui_set_flipped(
                    &app.gui,
                    !app.pos.white_to_move); // if it's black to move, flip so black bottom
            }
        }
        if (updated && sel.clicked && sel.to_sq >= 0)
        {
            Move   list[MAX_MOVES];
            size_t n      = chess_generate_legal_moves(&app.pos, list, MAX_MOVES);
            bool   moved  = false;
            Move   chosen = {0};
            for (size_t i = 0; i < n; i++)
            {
                if (list[i].from == sel.from_sq && list[i].to == sel.to_sq)
                {
                    chosen = list[i];
                    moved  = true;
                    break;
                }
            }
            sel.clicked = false;
            sel.from_sq = sel.to_sq = -1;
            sel.promo               = 0;
            if (!moved)
                continue;

            // 7. Compare with expected move
            char uci[8];
            move_to_uci(&chosen, uci);
            bool correct = (expected_player_move[0] && strncmp(uci, expected_player_move, 8) == 0);
            if (correct)
            {
                chess_make_move(&app.pos, &chosen);
                position_push_move_text(&app.pos, &chosen, line_uci, sizeof(line_uci));
                (void)snprintf(status, sizeof(status), "Correct");
                awaiting_player = false; // engine move next
            }
            else
            {
                // log mistake: 1) save all moves that lead to mistake, 2) allow revisit later, 3)
                // reset position
                char fen[256];
                chess_to_fen(&app.pos, fen, sizeof(fen));
                MistakeEntry entry = {
                    .fen       = fen,
                    .best_move = expected_player_move,
                    .line      = line_uci,
                };
                mistakes_add(&app.mistakes, &entry);
                (void)mistakes_save(&app.mistakes, mistake_file_path());
                (void)snprintf(status, sizeof(status), "Wrong, best was %s", expected_player_move);
                // redo best move to show
                Move best;
                if (parse_uci_move(expected_player_move, &app.pos, &best))
                {
                    chess_make_move(&app.pos, &best);
                    position_push_move_text(&app.pos, &best, line_uci, sizeof(line_uci));
                }
                gui_draw(&app.gui, app.pos.board, &sel, status);
                SDL_Delay(600);
                // reset to start
                chess_init_start(&app.pos);
                line_uci[0] = '\0';
                // randomize side again
                player_is_white = rand() % 2 == 0;
                gui_set_flipped(&app.gui, !player_is_white);
                awaiting_player         = player_is_white;
                expected_player_move[0] = '\0';
            }
        }

        SDL_Delay(10);
    }

    mistakes_save(&app.mistakes, mistake_file_path());
    gui_destroy(&app.gui);
    engine_stop(&app.engine);
    mistakes_free(&app.mistakes);
    return 0;
}
