// dbexport.cpp - MySQL export implementation for mw4print
//
// Included directly from ChildView.cpp (same pattern as recscore.cpp).
//

#include "dbexport.h"

// ---- Config default values -----------------------------------------------

SDBConfig g_dbConfig = {
    false,          // bEnabled
    "localhost",    // szHost
    3306,           // nPort
    "firestorm_stats", // szDatabase
    "mw4",          // szUsername
    "",             // szPassword
    false           // bExportEvents
};

// ---- MySQL late binding --------------------------------------------------
//
// We load libmysql.dll at runtime so the exe does not hard-link against it.
// All function pointers are NULL until DB_LoadMySQL() succeeds.

static HMODULE g_hMySQL = NULL;

typedef void*           MYSQLH;   // opaque MYSQL* handle
typedef unsigned long   MYULONG;
typedef unsigned __int64 MYULLONG;

// MYSQL_OPT_CONNECT_TIMEOUT = 0
#define MY_OPT_CONNECT_TIMEOUT 0

typedef MYSQLH    (__cdecl *PFN_mysql_init)          (MYSQLH);
typedef MYSQLH    (__cdecl *PFN_mysql_real_connect)  (MYSQLH, const char*, const char*, const char*, const char*, unsigned int, const char*, unsigned long);
typedef void      (__cdecl *PFN_mysql_options)       (MYSQLH, int, const char*);
typedef int       (__cdecl *PFN_mysql_real_query)    (MYSQLH, const char*, unsigned long);
typedef MYULLONG  (__cdecl *PFN_mysql_insert_id)     (MYSQLH);
typedef void      (__cdecl *PFN_mysql_close)         (MYSQLH);
typedef const char* (__cdecl *PFN_mysql_error)       (MYSQLH);
typedef MYULONG   (__cdecl *PFN_mysql_escape_string) (char*, const char*, MYULONG);

static PFN_mysql_init          pfn_init          = NULL;
static PFN_mysql_real_connect  pfn_real_connect  = NULL;
static PFN_mysql_options       pfn_options       = NULL;
static PFN_mysql_real_query    pfn_real_query    = NULL;
static PFN_mysql_insert_id     pfn_insert_id     = NULL;
static PFN_mysql_close         pfn_close         = NULL;
static PFN_mysql_error         pfn_error         = NULL;
static PFN_mysql_escape_string pfn_escape_string = NULL;

static bool DB_LoadMySQL()
{
    if (g_hMySQL)
        return true;

    // Try app directory first, then system path.
    char szPath[MAX_PATH];
    sprintf(szPath, "%s\\libmysql.dll", AssetsDirectory1);
    g_hMySQL = LoadLibrary(szPath);
    if (!g_hMySQL)
        g_hMySQL = LoadLibrary("libmysql.dll");
    if (!g_hMySQL)
        return false;

#define GETPROC(name, pfn) pfn = (PFN_##name)GetProcAddress(g_hMySQL, #name); if (!pfn) { FreeLibrary(g_hMySQL); g_hMySQL=NULL; return false; }
    GETPROC(mysql_init,          pfn_init)
    GETPROC(mysql_real_connect,  pfn_real_connect)
    GETPROC(mysql_options,       pfn_options)
    GETPROC(mysql_real_query,    pfn_real_query)
    GETPROC(mysql_insert_id,     pfn_insert_id)
    GETPROC(mysql_close,         pfn_close)
    GETPROC(mysql_error,         pfn_error)
    GETPROC(mysql_escape_string, pfn_escape_string)
#undef GETPROC

    return true;
}

// ---- Helpers -------------------------------------------------------------

// Open a connection using current config.  Returns handle on success, NULL
// on failure.  Caller must call pfn_close on the returned handle.
static MYSQLH DB_Connect(char* pszError, int nErrBufSize)
{
    if (!DB_LoadMySQL()) {
        if (pszError)
            strncpy(pszError, "libmysql.dll not found.  Install MySQL Connector/C and place libmysql.dll next to mw4print.exe.", nErrBufSize - 1);
        return NULL;
    }

    MYSQLH h = pfn_init(NULL);
    if (!h) {
        if (pszError)
            strncpy(pszError, "mysql_init failed (out of memory?)", nErrBufSize - 1);
        return NULL;
    }

    // Short connect timeout so the app doesn't hang if server is unreachable.
    int nTimeout = 5;
    pfn_options(h, MY_OPT_CONNECT_TIMEOUT, (const char*)&nTimeout);

    MYSQLH conn = pfn_real_connect(h,
        g_dbConfig.szHost,
        g_dbConfig.szUsername,
        g_dbConfig.szPassword,
        g_dbConfig.szDatabase,
        (unsigned int)g_dbConfig.nPort,
        NULL, 0);

    if (!conn) {
        if (pszError) {
            strncpy(pszError, pfn_error(h), nErrBufSize - 1);
            pszError[nErrBufSize - 1] = '\0';
        }
        pfn_close(h);
        return NULL;
    }

    return h;
}

// Execute a query string; return true on success.
static bool DB_Exec(MYSQLH h, const char* pszSQL)
{
    return pfn_real_query(h, pszSQL, (unsigned long)strlen(pszSQL)) == 0;
}

// Escape a string for use inside SQL single-quoted literals.
// Writes into pszOut (must be at least 2*len+1 bytes).
static void DB_Escape(char* pszOut, const char* pszIn)
{
    if (pfn_escape_string)
        pfn_escape_string(pszOut, pszIn, (MYULONG)strlen(pszIn));
    else {
        // Fallback: manual escape of ' and backslash
        char* dst = pszOut;
        for (const char* src = pszIn; *src; ++src) {
            if (*src == '\'' || *src == '\\')
                *dst++ = '\\';
            *dst++ = *src;
        }
        *dst = '\0';
    }
}

// ---- CREATE TABLE statements (run on first connect) ----------------------

static const char* s_szCreateMatch =
"CREATE TABLE IF NOT EXISTS `match` ("
"  `match_id`       INT UNSIGNED NOT NULL AUTO_INCREMENT,"
"  `match_time`     DATETIME     NOT NULL,"
"  `server_name`    VARCHAR(256) NOT NULL DEFAULT '',"
"  `server_ip`      VARCHAR(64)  NOT NULL DEFAULT '',"
"  `map_name`       VARCHAR(128) NOT NULL DEFAULT '',"
"  `scenario_name`  VARCHAR(128) NOT NULL DEFAULT '',"
"  `game_type`      INT          NOT NULL DEFAULT 0,"
"  `game_type_name` VARCHAR(64)  NOT NULL DEFAULT '',"
"  `is_team_game`   TINYINT(1)   NOT NULL DEFAULT 0,"
"  `player_count`   INT          NOT NULL DEFAULT 0,"
"  `team_count`     INT          NOT NULL DEFAULT 0,"
"  `team_scores`    VARCHAR(64)  NOT NULL DEFAULT '',"
"  `game_length`    INT          NOT NULL DEFAULT 0,"
"  `kill_limit`     INT          NOT NULL DEFAULT 0,"
"  `respawn_limit`  INT          NOT NULL DEFAULT 0,"
"  `friendly_fire`  INT          NOT NULL DEFAULT 0,"
"  `heat_on`        TINYINT(1)   NOT NULL DEFAULT 0,"
"  `armor_mode`     TINYINT(1)   NOT NULL DEFAULT 0,"
"  `unlimited_ammo` TINYINT(1)   NOT NULL DEFAULT 0,"
"  `is_night`       TINYINT(1)   NOT NULL DEFAULT 0,"
"  `min_tonnage`    INT          NOT NULL DEFAULT 0,"
"  `max_tonnage`    INT          NOT NULL DEFAULT 0,"
"  PRIMARY KEY (`match_id`)"
") ENGINE=InnoDB DEFAULT CHARSET=utf8";

static const char* s_szCreatePlayerResult =
"CREATE TABLE IF NOT EXISTS `player_result` ("
"  `result_id`   INT UNSIGNED NOT NULL AUTO_INCREMENT,"
"  `match_id`    INT UNSIGNED NOT NULL,"
"  `player_name` VARCHAR(64)  NOT NULL DEFAULT '',"
"  `mech_name`   VARCHAR(64)  NOT NULL DEFAULT '',"
"  `team`        INT          NOT NULL DEFAULT 0,"
"  `score`       INT          NOT NULL DEFAULT 0,"
"  `kills`       INT          NOT NULL DEFAULT 0,"
"  `deaths`      INT          NOT NULL DEFAULT 0,"
"  `is_bot`      TINYINT(1)   NOT NULL DEFAULT 0,"
"  `pilot_decal` INT          NOT NULL DEFAULT 0,"
"  PRIMARY KEY (`result_id`),"
"  KEY `match_id` (`match_id`)"
") ENGINE=InnoDB DEFAULT CHARSET=utf8";

static const char* s_szCreatePVP =
"CREATE TABLE IF NOT EXISTS `pvp` ("
"  `pvp_id`      INT UNSIGNED NOT NULL AUTO_INCREMENT,"
"  `match_id`    INT UNSIGNED NOT NULL,"
"  `attacker`    VARCHAR(64)  NOT NULL DEFAULT '',"
"  `victim`      VARCHAR(64)  NOT NULL DEFAULT '',"
"  `kills`       INT          NOT NULL DEFAULT 0,"
"  `score`       INT          NOT NULL DEFAULT 0,"
"  PRIMARY KEY (`pvp_id`),"
"  KEY `match_id` (`match_id`)"
") ENGINE=InnoDB DEFAULT CHARSET=utf8";

static const char* s_szCreateEvent =
"CREATE TABLE IF NOT EXISTS `event` ("
"  `event_id`   INT UNSIGNED NOT NULL AUTO_INCREMENT,"
"  `match_id`   INT UNSIGNED NOT NULL,"
"  `event_type` INT          NOT NULL DEFAULT 0,"
"  `actor`      VARCHAR(64)  NOT NULL DEFAULT '',"
"  `target`     VARCHAR(64)  NOT NULL DEFAULT '',"
"  `weapon`     INT          NOT NULL DEFAULT 0,"
"  `body_zone`  INT          NOT NULL DEFAULT 0,"
"  `param`      INT          NOT NULL DEFAULT 0,"
"  PRIMARY KEY (`event_id`),"
"  KEY `match_id` (`match_id`)"
") ENGINE=InnoDB DEFAULT CHARSET=utf8";

// Map CBucketManager::Bucket_Type int to a game_type name string.
static const char* GameTypeName(int n)
{
    switch (n) {
    case 0:  return "StockDestruction";
    case 1:  return "StockTeamDestruction";
    case 2:  return "StockAttrition";
    case 3:  return "StockTeamAttrition";
    case 4:  return "StockCaptureTheFlag";
    case 5:  return "StockKingOfTheHill";
    case 6:  return "StockTeamKingOfTheHill";
    case 7:  return "StockTerritories";
    case 8:  return "StockStealTheBacon";
    case 9:  return "StockCaptureBase";
    case 10: return "StockDestroyObjective";
    case 11: return "StockEscort";
    case 12: return "StockMasterTrial";
    case 13: return "StockSiegeAssault";
    case 14: return "StockCampaign";
    case 15: return "CustomDestruction";
    case 16: return "CustomTeamDestruction";
    case 17: return "CustomAttrition";
    case 18: return "CustomTeamAttrition";
    case 19: return "CustomCaptureTheFlag";
    case 20: return "CustomKingOfTheHill";
    case 21: return "CustomTeamKingOfTheHill";
    case 22: return "CustomTerritories";
    case 23: return "CustomStealTheBacon";
    case 24: return "CustomCaptureBase";
    case 25: return "CustomDestroyObjective";
    case 26: return "CustomEscort";
    case 27: return "CustomMasterTrial";
    case 28: return "CustomSiegeAssault";
    case 29: return "CustomCampaign";
    default: return "Unknown";
    }
}

// Resolve a ReplicatorID to a player name by scanning aRSOs[].
static const char* ResolvePlayerName(
    const ReplicatorID& id,
    PCRecScoreObject    aRSOs[],
    int                 nPlayers)
{
    for (int i = 0; i < nPlayers; i++) {
        if (aRSOs[i]->m_who == id)
            return aRSOs[i]->GetName();
    }
    return "";
}

// ---- Config I/O ----------------------------------------------------------

static void DB_GetINIPath(char* pszOut)
{
    sprintf(pszOut, "%s\\mw4print.ini", AssetsDirectory1);
}

void DB_LoadConfig()
{
    char szINI[MAX_PATH];
    DB_GetINIPath(szINI);

    const char* sec = "MySQLExport";
    g_dbConfig.bEnabled       = !!GetPrivateProfileInt(sec, "Enabled",   0,    szINI);
    GetPrivateProfileString(sec, "Host",     "localhost",       g_dbConfig.szHost,     sizeof(g_dbConfig.szHost),     szINI);
    g_dbConfig.nPort          =   GetPrivateProfileInt(sec, "Port",     3306,  szINI);
    GetPrivateProfileString(sec, "Database", "firestorm_stats", g_dbConfig.szDatabase, sizeof(g_dbConfig.szDatabase), szINI);
    GetPrivateProfileString(sec, "Username", "mw4",             g_dbConfig.szUsername, sizeof(g_dbConfig.szUsername), szINI);
    GetPrivateProfileString(sec, "Password", "",                g_dbConfig.szPassword, sizeof(g_dbConfig.szPassword), szINI);
    g_dbConfig.bExportEvents  = !!GetPrivateProfileInt(sec, "ExportEvents", 0, szINI);
}

void DB_SaveConfig()
{
    char szINI[MAX_PATH];
    DB_GetINIPath(szINI);

    const char* sec = "MySQLExport";
    char szBuf[32];

    WritePrivateProfileString(sec, "Enabled",      g_dbConfig.bEnabled      ? "1" : "0", szINI);
    WritePrivateProfileString(sec, "Host",         g_dbConfig.szHost,                    szINI);
    _itoa(g_dbConfig.nPort, szBuf, 10);
    WritePrivateProfileString(sec, "Port",         szBuf,                                szINI);
    WritePrivateProfileString(sec, "Database",     g_dbConfig.szDatabase,                szINI);
    WritePrivateProfileString(sec, "Username",     g_dbConfig.szUsername,                szINI);
    WritePrivateProfileString(sec, "Password",     g_dbConfig.szPassword,                szINI);
    WritePrivateProfileString(sec, "ExportEvents", g_dbConfig.bExportEvents  ? "1" : "0", szINI);
}

// ---- Test connection -----------------------------------------------------

bool DB_TestConnection(char* pszError, int nErrBufSize)
{
    if (pszError && nErrBufSize > 0)
        pszError[0] = '\0';

    MYSQLH h = DB_Connect(pszError, nErrBufSize);
    if (!h)
        return false;

    bool bOK = DB_Exec(h, "SELECT 1");
    if (!bOK && pszError) {
        strncpy(pszError, pfn_error(h), nErrBufSize - 1);
        pszError[nErrBufSize - 1] = '\0';
    }
    pfn_close(h);
    return bOK;
}

// ---- Main export ---------------------------------------------------------

bool DB_ExportMatch(
    const CRecScoreFull&           rsf,
    PCRecScoreObject               aRSOs[],
    int                            nPlayers,
    const int                      naTeams[8],
    int                            nTeamCount,
    const MWNetMissionParameters&  params)
{
    char szError[512];
    MYSQLH h = DB_Connect(szError, sizeof(szError));
    if (!h)
        return false;

    char szSQL[4096];
    char szEsc[512];
    bool bOK = true;

    // Ensure tables exist (silent if already present).
    DB_Exec(h, s_szCreateMatch);
    DB_Exec(h, s_szCreatePlayerResult);
    DB_Exec(h, s_szCreatePVP);
    if (g_dbConfig.bExportEvents)
        DB_Exec(h, s_szCreateEvent);

    // ---- Insert match row ------------------------------------------------

    // Build team_scores as comma-separated string e.g. "100,80,0,0,0,0,0,0"
    char szTeamScores[64];
    sprintf(szTeamScores, "%d,%d,%d,%d,%d,%d,%d,%d",
        rsf.m_naTeamScores[0], rsf.m_naTeamScores[1],
        rsf.m_naTeamScores[2], rsf.m_naTeamScores[3],
        rsf.m_naTeamScores[4], rsf.m_naTeamScores[5],
        rsf.m_naTeamScores[6], rsf.m_naTeamScores[7]);

    char szServerNameE[512], szServerIPE[512], szMapE[256], szScenarioE[256], szGameTypeNameE[128];
    DB_Escape(szServerNameE,   params.m_serverName);
    DB_Escape(szServerIPE,     params.m_serverIP);
    DB_Escape(szMapE,          params.m_mapName);
    DB_Escape(szScenarioE,     params.m_scenarioName);
    DB_Escape(szGameTypeNameE, GameTypeName(params.m_ruleType));

    sprintf(szSQL,
        "INSERT INTO `match` "
        "(match_time, server_name, server_ip, map_name, scenario_name, "
        " game_type, game_type_name, is_team_game, player_count, team_count, "
        " team_scores, game_length, kill_limit, respawn_limit, friendly_fire, "
        " heat_on, armor_mode, unlimited_ammo, is_night, min_tonnage, max_tonnage) "
        "VALUES ("
        "'%04d-%02d-%02d %02d:%02d:%02d',"  // match_time
        "'%s','%s','%s','%s',"              // server_name, server_ip, map_name, scenario_name
        "%d,'%s',%d,%d,%d,"                // game_type, game_type_name, is_team_game, player_count, team_count
        "'%s',"                             // team_scores
        "%d,%d,%d,%d,"                     // game_length, kill_limit, respawn_limit, friendly_fire
        "%d,%d,%d,%d,%d,%d"               // heat_on, armor_mode, unlimited_ammo, is_night, min_tonnage, max_tonnage
        ")",
        rsf.m_SysTime.wYear, rsf.m_SysTime.wMonth,  rsf.m_SysTime.wDay,
        rsf.m_SysTime.wHour, rsf.m_SysTime.wMinute, rsf.m_SysTime.wSecond,
        szServerNameE, szServerIPE, szMapE, szScenarioE,
        params.m_ruleType, szGameTypeNameE,
        rsf.m_bTeamGame ? 1 : 0, nPlayers, nTeamCount,
        szTeamScores,
        params.m_gameLength,
        params.m_killLimit     ? params.m_killLimitNumber    : 0,
        params.m_respawnLimit  ? params.m_respawnLimitNumber : 0,
        params.m_friendlyFirePercentage,
        params.m_heatOn        ? 1 : 0,
        params.m_armormodeOn   ? 1 : 0,
        params.m_unlimitedAmmo ? 1 : 0,
        params.m_isNight       ? 1 : 0,
        params.m_minimumTonnage,
        params.m_maximumTonnage);

    bOK = DB_Exec(h, szSQL);
    if (!bOK) {
        pfn_close(h);
        return false;
    }

    MYULLONG nMatchID = pfn_insert_id(h);

    // ---- Insert player_result rows ---------------------------------------

    for (int i = 0; i < nPlayers && bOK; i++) {
        const CRecScoreObject& rso = *aRSOs[i];
        char szNameE[128], szMechE[128];
        DB_Escape(szNameE, rso.GetName());
        DB_Escape(szMechE, rso.GetMech());

        sprintf(szSQL,
            "INSERT INTO `player_result` "
            "(match_id, player_name, mech_name, team, score, kills, deaths, is_bot, pilot_decal) "
            "VALUES (%I64u,'%s','%s',%d,%d,%d,%d,%d,%d)",
            nMatchID,
            szNameE, szMechE,
            rso.m_nPilotTeam,
            rso.m_nScore,
            rso.m_nKills,
            rso.m_nDeaths,
            rso.m_bBOT ? 1 : 0,
            rso.m_nPilotDecal);

        bOK = DB_Exec(h, szSQL);
    }

    // ---- Insert pvp rows -------------------------------------------------

    for (int p = 0; p < rsf.m_nPVPs && bOK; p++) {
        const CRecScoreFull::SPVP_Rec& pvp = rsf.m_pPVPs[p];
        const char* pszAttacker = ResolvePlayerName(pvp.m_Inf, aRSOs, nPlayers);
        const char* pszVictim   = ResolvePlayerName(pvp.m_Rcv, aRSOs, nPlayers);

        char szAttE[128], szVicE[128];
        DB_Escape(szAttE, pszAttacker);
        DB_Escape(szVicE, pszVictim);

        sprintf(szSQL,
            "INSERT INTO `pvp` (match_id, attacker, victim, kills, score) "
            "VALUES (%I64u,'%s','%s',%d,%d)",
            nMatchID, szAttE, szVicE, pvp.m_nKills, pvp.m_nScore);

        bOK = DB_Exec(h, szSQL);
    }

    // ---- Insert event rows (optional) ------------------------------------

    if (bOK && g_dbConfig.bExportEvents) {
        // Walk the CRecScorePack linked list.
        const CRecScorePack* pPack = rsf.m_pScorePackStart;
        while (pPack && bOK) {
            for (int e = 0; e < pPack->m_nCount && bOK; e++) {
                const SRecScore& rs = pPack->m_a[e];
                const char* pszActor  = ResolvePlayerName(rs.m_who,  aRSOs, nPlayers);
                const char* pszTarget = ResolvePlayerName(rs.m_whom, aRSOs, nPlayers);
                char szActE[128], szTgtE[128];
                DB_Escape(szActE, pszActor);
                DB_Escape(szTgtE, pszTarget);

                sprintf(szSQL,
                    "INSERT INTO `event` (match_id, event_type, actor, target, weapon, body_zone, param) "
                    "VALUES (%I64u,%d,'%s','%s',%d,%d,%d)",
                    nMatchID,
                    rs.m_nType,
                    szActE, szTgtE,
                    rs.m_nBy,
                    rs.m_Where.m_xWhere,
                    rs.m_nParam);

                bOK = DB_Exec(h, szSQL);
            }
            pPack = pPack->m_pNext;
        }
    }

    pfn_close(h);
    return bOK;
}
