#include "redump_dat.h"

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static int redump_ascii_equal(const char *a, const char *b) {
    unsigned char ca, cb;
    if (!a || !b)
        return 0;
    while (*a && *b) {
        ca = (unsigned char)tolower((unsigned char)*a++);
        cb = (unsigned char)tolower((unsigned char)*b++);
        if (ca != cb)
            return 0;
    }
    return *a == '\0' && *b == '\0';
}

static int redump_extract_attr(const char *line, const char *name, char *out, size_t out_size) {
    char pattern[64];
    const char *start;
    const char *end;
    size_t len;

    if (!line || !name || !out || out_size == 0)
        return 0;
    snprintf(pattern, sizeof(pattern), "%s=\"", name);
    start = strstr(line, pattern);
    if (!start)
        return 0;
    start += strlen(pattern);
    end = strchr(start, '"');
    if (!end)
        return 0;
    len = (size_t)(end - start);
    if (len >= out_size)
        len = out_size - 1;
    memcpy(out, start, len);
    out[len] = '\0';
    return 1;
}

static void redump_xml_decode(char *text) {
    struct entity_s { const char *encoded; const char decoded; };
    static const struct entity_s entities[] = {
        {"&amp;", '&'}, {"&quot;", '"'}, {"&apos;", '\''},
        {"&lt;", '<'}, {"&gt;", '>'}
    };
    size_t i;
    if (!text)
        return;
    for (i = 0; i < sizeof(entities) / sizeof(entities[0]); i++) {
        char *p;
        size_t encoded_len = strlen(entities[i].encoded);
        while ((p = strstr(text, entities[i].encoded)) != NULL) {
            *p = entities[i].decoded;
            memmove(p + 1, p + encoded_len, strlen(p + encoded_len) + 1);
        }
    }
}

static void redump_store_candidate(
    redump_verify_result *result,
    const char *game_name,
    const char *rom_name,
    const char *size_text,
    const char *crc32,
    const char *md5,
    const char *sha1,
    int size_match,
    int crc32_match,
    int md5_match,
    int sha1_match,
    int hash_matches
) {
    if (!result)
        return;
    result->candidate_available = 1;
    result->candidate_hash_matches = hash_matches;
    result->size_status = size_match ? REDUMP_FIELD_PASS : REDUMP_FIELD_FAIL;
    result->crc32_status = crc32_match ? REDUMP_FIELD_PASS : REDUMP_FIELD_FAIL;
    result->md5_status = md5_match ? REDUMP_FIELD_PASS : REDUMP_FIELD_FAIL;
    result->sha1_status = sha1_match ? REDUMP_FIELD_PASS : REDUMP_FIELD_FAIL;
    snprintf(result->game_name, sizeof(result->game_name), "%s",
             (game_name && game_name[0]) ? game_name : ((rom_name && rom_name[0]) ? rom_name : ""));
    snprintf(result->rom_name, sizeof(result->rom_name), "%s", rom_name ? rom_name : "");
    snprintf(result->expected_size, sizeof(result->expected_size), "%s", size_text ? size_text : "");
    snprintf(result->expected_crc32, sizeof(result->expected_crc32), "%s", crc32 ? crc32 : "");
    snprintf(result->expected_md5, sizeof(result->expected_md5), "%s", md5 ? md5 : "");
    snprintf(result->expected_sha1, sizeof(result->expected_sha1), "%s", sha1 ? sha1 : "");
}

void redump_verify_result_init(redump_verify_result *result) {
    if (!result)
        return;
    memset(result, 0, sizeof(*result));
    result->status = REDUMP_VERIFY_NOT_RUN;
    result->size_status = REDUMP_FIELD_NOT_AVAILABLE;
    result->crc32_status = REDUMP_FIELD_NOT_AVAILABLE;
    result->md5_status = REDUMP_FIELD_NOT_AVAILABLE;
    result->sha1_status = REDUMP_FIELD_NOT_AVAILABLE;
}

const char *redump_verify_status_string(redump_verify_status status) {
    switch (status) {
        case REDUMP_VERIFY_MATCH: return "MATCH";
        case REDUMP_VERIFY_NO_MATCH: return "NO MATCH";
        case REDUMP_VERIFY_DAT_NOT_FOUND: return "DAT NOT FOUND";
        case REDUMP_VERIFY_DAT_READ_ERROR: return "DAT READ ERROR";
        case REDUMP_VERIFY_INVALID_INPUT: return "INVALID INPUT";
        default: return "NOT RUN";
    }
}

const char *redump_field_status_string(redump_field_status status) {
    switch (status) {
        case REDUMP_FIELD_PASS: return "PASS";
        case REDUMP_FIELD_FAIL: return "FAIL";
        default: return "N/A";
    }
}

const char *redump_verify_overall_string(const redump_verify_result *result) {
    if (!result)
        return "NOT VERIFIED";
    if (result->status == REDUMP_VERIFY_MATCH)
        return "VERIFIED AGAINST REDUMP";
    if (result->status == REDUMP_VERIFY_NO_MATCH)
        return "NO EXACT MATCH";
    return "NOT VERIFIED";
}

const char *redump_verify_confidence_string(const redump_verify_result *result) {
    if (!result)
        return "Verification unavailable";
    if (result->status == REDUMP_VERIFY_MATCH)
        return "Exact archive match";
    if (result->status == REDUMP_VERIFY_NO_MATCH && result->candidate_available)
        return "Partial hash correlation; dump not verified";
    if (result->status == REDUMP_VERIFY_NO_MATCH)
        return "No archive identity established";
    if (result->status == REDUMP_VERIFY_INVALID_INPUT)
        return "Required evidence unavailable";
    return "Verification unavailable";
}

const char *redump_verify_match_kind_string(const redump_verify_result *result) {
    if (!result)
        return "none";
    if (result->status == REDUMP_VERIFY_MATCH)
        return "exact";
    if (result->candidate_available)
        return "closest";
    return "none";
}

int redump_verify_dat_file(
    const char *dat_path,
    uint64_t image_size,
    const char *crc32,
    const char *md5,
    const char *sha1,
    redump_verify_result *result
) {
    FILE *fp;
    char line[8192];
    char current_game[512] = "";
    int best_hash_matches = 0;
    int best_size_match = 0;

    if (!result)
        return 0;
    redump_verify_result_init(result);

    if (dat_path)
        snprintf(result->dat_path, sizeof(result->dat_path), "%s", dat_path);
    if (!dat_path || !dat_path[0] || image_size == 0 || !crc32 || !md5 || !sha1 ||
        !crc32[0] || !md5[0] || !sha1[0]) {
        result->status = REDUMP_VERIFY_INVALID_INPUT;
        snprintf(result->detail, sizeof(result->detail),
                 "Output size plus complete CRC32, MD5, and SHA-1 values are required.");
        return 0;
    }

    fp = fopen(dat_path, "rb");
    if (!fp) {
        result->status = REDUMP_VERIFY_DAT_NOT_FOUND;
        snprintf(result->detail, sizeof(result->detail), "Could not open DAT file.");
        return 0;
    }

    while (fgets(line, sizeof(line), fp)) {
        if (strstr(line, "<game ") != NULL) {
            if (redump_extract_attr(line, "name", current_game, sizeof(current_game)))
                redump_xml_decode(current_game);
            continue;
        }

        if (strstr(line, "<rom ") != NULL) {
            char rom_name[512] = "";
            char size_text[32] = "";
            char entry_crc[16] = "";
            char entry_md5[40] = "";
            char entry_sha1[48] = "";
            uint64_t entry_size;
            int size_match;
            int crc32_match;
            int md5_match;
            int sha1_match;
            int hash_matches;

            if (!redump_extract_attr(line, "size", size_text, sizeof(size_text)) ||
                !redump_extract_attr(line, "crc", entry_crc, sizeof(entry_crc)) ||
                !redump_extract_attr(line, "md5", entry_md5, sizeof(entry_md5)) ||
                !redump_extract_attr(line, "sha1", entry_sha1, sizeof(entry_sha1)))
                continue;

            redump_extract_attr(line, "name", rom_name, sizeof(rom_name));
            redump_xml_decode(rom_name);
            entry_size = (uint64_t)strtoull(size_text, NULL, 10);

            result->entries_scanned++;
            size_match = (entry_size == image_size);
            crc32_match = redump_ascii_equal(crc32, entry_crc);
            md5_match = redump_ascii_equal(md5, entry_md5);
            sha1_match = redump_ascii_equal(sha1, entry_sha1);
            hash_matches = crc32_match + md5_match + sha1_match;

            if (size_match) result->size_matches++;
            if (crc32_match) result->crc32_matches++;
            if (md5_match) result->md5_matches++;
            if (sha1_match) result->sha1_matches++;

            if (size_match && crc32_match && md5_match && sha1_match) {
                result->exact_matches++;
                if (result->status != REDUMP_VERIFY_MATCH) {
                    result->status = REDUMP_VERIFY_MATCH;
                    redump_store_candidate(result, current_game, rom_name, size_text,
                                           entry_crc, entry_md5, entry_sha1,
                                           1, 1, 1, 1, 3);
                    snprintf(result->detail, sizeof(result->detail),
                             "Size, CRC32, MD5, and SHA-1 matched.");
                }
                continue;
            }

            if (result->status != REDUMP_VERIFY_MATCH && hash_matches > 0 &&
                (hash_matches > best_hash_matches ||
                 (hash_matches == best_hash_matches && size_match > best_size_match))) {
                best_hash_matches = hash_matches;
                best_size_match = size_match;
                redump_store_candidate(result, current_game, rom_name, size_text,
                                       entry_crc, entry_md5, entry_sha1,
                                       size_match, crc32_match, md5_match, sha1_match,
                                       hash_matches);
            }
        }
    }

    if (ferror(fp)) {
        fclose(fp);
        result->status = REDUMP_VERIFY_DAT_READ_ERROR;
        snprintf(result->detail, sizeof(result->detail),
                 "An I/O error occurred while reading the DAT file.");
        return 0;
    }
    fclose(fp);

    if (result->status == REDUMP_VERIFY_MATCH)
        return 1;

    result->status = REDUMP_VERIFY_NO_MATCH;
    if (result->candidate_available) {
        snprintf(result->detail, sizeof(result->detail),
                 "No exact entry matched; the closest hash-correlated candidate is shown.");
    } else {
        snprintf(result->detail, sizeof(result->detail),
                 "No entry matched all required fields and no hash-correlated candidate was found.");
    }
    return 0;
}
