import os
import json
import logging
from contextlib import contextmanager
import psycopg2
import psycopg2.pool
import psycopg2.extras

from net.macho_marshal import MachoObject, UniStr, long_type
from net.packets import get_filetime_now, datetime_to_filetime

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
CONFIG_PATH = os.path.join(BASE_DIR, "config", "server_config.json")
logger = logging.getLogger("dystropia")

_DB_POOL = None

def load_config():
    with open(CONFIG_PATH, "r") as f:
        return json.load(f)

def save_config(cfg):
    try:
        with open(CONFIG_PATH, "w") as cf:
            json.dump(cfg, cf, indent=4)
    except Exception as e:
        logger.warning("[!] Warning saving config: %s", e)

def init_db_pool(cfg):
    global _DB_POOL
    if _DB_POOL is not None:
        return _DB_POOL
    db_cfg = cfg.get("db", {})
    try:
        _DB_POOL = psycopg2.pool.ThreadedConnectionPool(
            minconn=2,
            maxconn=16,
            dbname=db_cfg.get("dbname", "ericgw_dystropia"),
            user=db_cfg.get("user", "ericgw_eve"),
            password=db_cfg.get("password", "DystropiaEve2026_db"),
            host=db_cfg.get("host", "127.0.0.1"),
            port=db_cfg.get("port", 5432),
        )
    except Exception:
        _DB_POOL = psycopg2.pool.ThreadedConnectionPool(
            minconn=1,
            maxconn=10,
            dbname=db_cfg.get("dbname", "ericgw_dystropia"),
            user="postgres",
        )
    return _DB_POOL

def get_db_conn(cfg=None):
    """Backward-compatible helper for manage_users.py and utility scripts."""
    if cfg is None:
        cfg = load_config()
    db_cfg = cfg.get("db", {})
    try:
        return psycopg2.connect(
            dbname=db_cfg.get("dbname", "ericgw_dystropia"),
            user=db_cfg.get("user", "ericgw_eve"),
            password=db_cfg.get("password", "DystropiaEve2026_db"),
            host=db_cfg.get("host", "127.0.0.1"),
            port=db_cfg.get("port", 5432),
        )
    except Exception:
        return psycopg2.connect(dbname=db_cfg.get("dbname", "ericgw_dystropia"), user="postgres")

@contextmanager
def pooled_conn(cfg):
    pool = init_db_pool(cfg)
    conn = pool.getconn()
    try:
        yield conn
        conn.commit()
    except Exception:
        conn.rollback()
        raise
    finally:
        pool.putconn(conn)

def db_authenticate_user(cfg, raw_username):
    uname = raw_username.val if isinstance(raw_username, UniStr) else str(raw_username)
    uname = uname.strip()
    if not uname:
        return None
    with pooled_conn(cfg) as conn:
        with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
            cur.execute("SELECT * FROM users WHERE LOWER(user_name) = LOWER(%s)", (uname,))
            user = cur.fetchone()
            if not user:
                logger.warning("[Auth] Unknown user_name=%r - account does not exist!", uname)
                return None
            logger.info("[DB] Loaded existing user %r (user_id=%d) from PostgreSQL", user["user_name"], user["user_id"])
            return dict(user)

def db_verify_or_set_password(cfg, user_id, candidate_hash):
    if not candidate_hash:
        return False
    raw_bytes = candidate_hash.encode("latin1") if isinstance(candidate_hash, str) else bytes(candidate_hash)
    with pooled_conn(cfg) as conn:
        with conn.cursor() as cur:
            cur.execute("SELECT password_hash FROM users WHERE user_id = %s", (user_id,))
            row = cur.fetchone()
            stored = bytes(row[0]) if (row and row[0] is not None) else None
            if stored is None:
                cur.execute("UPDATE users SET password_hash = %s WHERE user_id = %s", (psycopg2.Binary(raw_bytes), user_id))
                logger.info("[Auth] TOFU: Initialized and locked password_hash (%d bytes) for user_id=%d!", len(raw_bytes), user_id)
                return True
            match = (stored == raw_bytes)
            logger.info("[Auth] Password verification for user_id=%d: %s", user_id, "SUCCESS" if match else "FAILED")
            return match

def db_get_character_skills(session):
    if session.skills_cache is not None:
        return session.skills_cache

    skills_dict = {}
    free_sp = 5000000
    total_sp = 0
    char_id = session.char_id
    try:
        with pooled_conn(session.cfg) as conn:
            with conn.cursor() as cur:
                cur.execute(
                    "SELECT type_id, skill_level, skill_points, skill_rank "
                    "FROM character_skills WHERE character_id = %s",
                    (char_id,)
                )
                for tid, lvl, sp, rank in cur.fetchall():
                    tid = int(tid)
                    lvl = int(lvl)
                    sp = int(sp)
                    rank = float(rank) if (float(rank) % 1 != 0) else int(float(rank))
                    total_sp += sp
                    skills_dict[tid] = MachoObject("utillib.KeyVal", {
                        "typeID": tid,
                        "trainedSkillLevel": lvl,
                        "trainedSkillPoints": sp,
                        "effectiveSkillLevel": lvl,
                        "virtualSkillLevel": None,
                        "skillRank": rank,
                        "skillPoints": sp,
                        "skillLevel": lvl,
                        "_typeID": tid,
                        "_trainedSkillLevel": lvl,
                        "_skillPoints": sp,
                        "_skillRank": rank,
                        "_virtualSkillLevel": None,
                    })
                cur.execute(
                    "SELECT free_skill_points FROM character_skill_points WHERE character_id = %s",
                    (char_id,)
                )
                row = cur.fetchone()
                if row and row[0] is not None:
                    free_sp = int(row[0])
    except Exception as e:
        logger.warning("[DB] Skill fetch warning for char_id=%s: %s", char_id, e)

    session.skills_cache = (skills_dict, free_sp, total_sp)
    return session.skills_cache

def db_load_character_into_session(session, char_id):
    with pooled_conn(session.cfg) as conn:
        with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
            cur.execute("SELECT * FROM characters WHERE character_id = %s", (char_id,))
            c = cur.fetchone()
            if c:
                session.char_id = int(c["character_id"])
                session.char_name = str(c["character_name"])
                session.corp_id = int(c["corporation_id"])
                session.alliance_id = c["alliance_id"]
                session.station_id = int(c["station_id"])
                session.solarsystem_id = int(c["solar_system_id"])
                session.ship_type_id = int(c["ship_type_id"])
                session.gender = int(c["gender"])
                session.race_id = int(c["race_id"])
                session.bloodline_id = int(c["bloodline_id"])
                session.ancestry_id = int(c["ancestry_id"])
                session.skills_cache = None

def db_get_character_selection_data(cfg, user_id):
    now_ft = get_filetime_now()
    day_ticks = long_type(864000000000)
    hour_ticks = long_type(36000000000)

    with pooled_conn(cfg) as conn:
        with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
            cur.execute("SELECT * FROM users WHERE user_id = %s", (user_id,))
            u = cur.fetchone()
            cur.execute("SELECT * FROM characters WHERE user_id = %s ORDER BY character_id", (user_id,))
            chars = cur.fetchall()

    uname = u["user_name"] if u else "gm_eric"
    slots = int(u["character_slots"]) if u else 3

    user_row = MachoObject("utillib.KeyVal", {
        "characterSlots": slots,
        "maxCharacterSlots": slots,
        "userName": UniStr(uname),
        "creationDate": now_ft - day_ticks * 365,
        "subscriptionEndTime": now_ft + day_ticks * 365,
    })

    char_rows = []
    for c in chars:
        logger.info("[DB] Serving character %r (ID=%d, Balance=%s ISK, Ship=%d)",
                    c["character_name"], c["character_id"], c["balance"], c["ship_type_id"])
        char_rows.append(MachoObject("utillib.KeyVal", {
            "characterID": int(c["character_id"]),
            "characterName": UniStr(c["character_name"]),
            "deletePrepareDateTime": None,
            "gender": int(c["gender"]),
            "accessoryID": None, "beardID": None, "costumeID": None, "decoID": None,
            "eyebrowsID": None, "eyesID": None, "hairID": None, "lipstickID": None,
            "makeupID": None, "skinID": None, "backgroundID": None, "lightID": None,
            "headMorph1": None, "headMorph2": None,
            "bloodlineID": int(c["bloodline_id"]),
            "raceID": int(c["race_id"]),
            "ancestryID": int(c["ancestry_id"]),
            "schoolID": int(c["school_id"]),
            "departmentID": int(c["department_id"]),
            "fieldID": int(c["field_id"]),
            "specialityID": int(c["speciality_id"]),
            "SolarSystemID": int(c["solar_system_id"]),
            "solarSystemID": int(c["solar_system_id"]),
            "stationID": int(c["station_id"]),
            "locationID": int(c["location_id"]),
            "corporationID": int(c["corporation_id"]),
            "allianceID": c["alliance_id"],
            "factionID": c["faction_id"],
            "createDateTime": datetime_to_filetime(c.get("created_at")) - day_ticks * 100,
            "logoffDate": now_ft - hour_ticks,
            "securityStatus": float(c["security_status"]),
            "balance": float(c["balance"]),
            "balanceChange": 0.0,
            "skillPoints": int(c["skill_points"]),
            "shipTypeID": int(c["ship_type_id"]),
            "unreadMailCount": 0,
            "unallocatedSP": 0,
            "paperdollState": 0,
            "finishedSkills": 0,
            "skillTypeID": None, "toLevel": None, "trainingStartTime": None,
            "trainingEndTime": None, "queueEndTime": None, "finishSP": None,
            "trainedSP": None, "lockTypeID": None, "petitionMessage": None,
        }))

    return ([user_row], (None, None), char_rows, [])

def db_get_multi_owners(cfg, req_ids):
    with pooled_conn(cfg) as conn:
        with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
            cur.execute(
                "SELECT character_id, character_name, gender FROM characters WHERE character_id = ANY(%s)",
                (list(req_ids),)
            )
            found = {int(r["character_id"]): r for r in cur.fetchall()}

    header = ["ownerID", "ownerName", "typeID", "gender", "ownerNameID"]
    rows = []
    for oid in req_ids:
        if oid in found:
            rows.append([oid, UniStr(found[oid]["character_name"]), 1383, int(found[oid]["gender"]), None])
        elif oid == 216121397:
            rows.append([oid, UniStr("Game Masters"), 2, 0, None])
        elif oid == 434243723:
            rows.append([oid, UniStr("C C P Alliance"), 16159, 0, None])
        else:
            rows.append([oid, UniStr("Owner %s" % oid), 1383, 1, None])
    return (header, rows)

def db_get_multi_locations(cfg, req_ids):
    with pooled_conn(cfg) as conn:
        with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
            cur.execute("SELECT station_id, station_name, solar_system_id, solar_system_name FROM characters")
            loc_map = {}
            for r in cur.fetchall():
                loc_map[int(r["station_id"])] = (r["station_name"], int(r["solar_system_id"]))
                loc_map[int(r["solar_system_id"])] = (r["solar_system_name"], int(r["solar_system_id"]))

    header = ["locationID", "locationName", "solarSystemID", "x", "y", "z", "locationNameID"]
    rows = []
    for lid in req_ids:
        if lid in loc_map:
            lname, sys_id = loc_map[lid]
            rows.append([lid, UniStr(lname), sys_id, 0.0, 0.0, 0.0, None])
        elif lid == 60014842:
            rows.append([lid, UniStr("Polaris II - Polaris Corporation Plantation"), 30000380, 0.0, 0.0, 0.0, None])
        elif lid == 30000380:
            rows.append([lid, UniStr("Polaris"), 30000380, 0.0, 0.0, 0.0, None])
        else:
            rows.append([lid, UniStr("Dodixie IX - Moon 20 - Federation Navy Assembly Plant"), 30002659, 0.0, 0.0, 0.0, None])
    return (header, rows)

# --- PaperDoll State Management ---

DEFAULT_PAPERDOLL_STATE = {
    "hairDarkness": 0.27,
    "colors": [
        [5, 43, 0, 0.0, 0.0], [6, 29, 215, 0.0, 0.0], [7, 195, 0, 0.0, 0.0],
        [8, 86, 0, 0.9494, 0.0], [10, 99, 0, 0.4395, 0.0], [16, 179, 0, 0.6115, 0.0],
    ],
    "modifiers": [
        [3, 15201, None], [6, 15200, None], [7, 171, None], [9, 203, None],
        [10, 16608, None], [22, 239, None], [25, 184, None], [15, 64, None],
        [17, 131, None], [23, 411, None], [24, 202, None], [131, 15199, None],
    ],
    "sculpts": [
        [13, 0.0, -0.0155, 0.0], [18, 0.0, 0.2157, 0.0], [19, 0.0, 0.0, -0.1117],
        [22, -0.1336, 0.2425, 0.0], [23, 0.0, -0.2725, 0.0], [24, 0.0, 0.0, 0.053],
        [25, 0.0, 0.0, -0.5115], [26, 0.0, 0.0, -0.011], [27, 0.0, -0.446, 0.0],
        [29, 0.0, 0.0, -0.3093], [30, -0.3274, -0.5927, 0.0],
    ],
    "portrait": {
        "cameraX": 0.09127, "cameraY": 1.53818, "cameraZ": 1.36897,
        "cameraFieldOfView": 0.3, "cameraPoiX": 0.001, "cameraPoiY": 1.69381, "cameraPoiZ": 0.081488,
        "backgroundID": 31, "lightID": 10744, "lightColorID": 10752, "lightIntensity": 0.85,
        "portraitPoseNumber": 0.0, "headLookTargetX": 0.0, "headLookTargetY": 1.5, "headLookTargetZ": 1.0,
        "headTilt": 0.0, "orientChar": 0.0, "browLeftCurl": 0.5, "browLeftTighten": 0.0,
        "browLeftUpDown": 0.5, "browRightCurl": 0.5, "browRightTighten": 0.0, "browRightUpDown": 0.5,
        "eyeClose": 0.0, "eyesLookVertical": 0.5, "eyesLookHorizontal": 0.6, "squintLeft": 0.0,
        "squintRight": 0.0, "jawSideways": 0.5, "jawUp": 0.5, "puckerLips": 0.0,
        "frownLeft": 0.0, "frownRight": 0.0, "smileLeft": 0.0, "smileRight": 0.0,
    },
}

def _paperdoll_path(char_id=90000001):
    p = os.path.join(BASE_DIR, "paperdoll_%d.json" % char_id)
    if os.path.exists(p):
        return p
    return os.path.join(BASE_DIR, "paperdoll_90000001.json")

def load_paperdoll_state(char_id=90000001):
    path = _paperdoll_path(char_id)
    if os.path.exists(path):
        try:
            with open(path, "r") as f:
                return json.load(f)
        except Exception as e:
            logger.warning("[!] Warning loading paperdoll json: %s", e)
    return json.loads(json.dumps(DEFAULT_PAPERDOLL_STATE))

def save_paperdoll_from_rpc(char_id, doll_info, portrait_info):
    st = load_paperdoll_state(char_id)
    target_path = os.path.join(BASE_DIR, "paperdoll_%d.json" % char_id)
    try:
        if isinstance(doll_info, MachoObject) and isinstance(doll_info.state, dict):
            d = doll_info.state
            app = d.get("appearance")
            if isinstance(app, (list, tuple)) and len(app) > 0:
                st["hairDarkness"] = float(app[0])
            elif isinstance(app, MachoObject) and isinstance(app.state, dict):
                line = app.state.get("line", [0.27])
                if line:
                    st["hairDarkness"] = float(line[0])
            if "colors" in d and isinstance(d["colors"], (list, tuple)):
                st["colors"] = [list(c) for c in d["colors"] if isinstance(c, (list, tuple))]
            if "modifiers" in d and isinstance(d["modifiers"], (list, tuple)):
                mods = [list(m) for m in d["modifiers"] if isinstance(m, (list, tuple))]
                res_ids = {m[1] for m in mods if len(m) >= 2}
                if 16608 in res_ids or 15199 in res_ids or 15200 in res_ids:
                    mods = [m for m in mods if m[0] not in (3, 6, 10, 22, 25, 131)]
                    mods.extend([[3, 15201, None], [6, 15200, None], [10, 16608, None], [22, 239, None], [25, 184, None], [131, 15199, None]])
                st["modifiers"] = mods
            if "sculpts" in d and isinstance(d["sculpts"], (list, tuple)):
                st["sculpts"] = [list(s) for s in d["sculpts"] if isinstance(s, (list, tuple))]
        if isinstance(portrait_info, MachoObject) and isinstance(portrait_info.state, dict):
            p = portrait_info.state
            for k in list(st["portrait"].keys()):
                if k in p and p[k] is not None:
                    val = p[k]
                    st["portrait"][k] = int(val) if k in ("backgroundID", "lightID", "lightColorID") else float(val)
        with open(target_path, "w") as f:
            json.dump(st, f, indent=2)
        logger.info("[+] Saved updated PaperDoll DNA & Portrait camera state to %s", target_path)
    except Exception as e:
        logger.error("[!] Error saving paperdoll state: %s", e)

def build_paperdoll_dna_keyval(char_id=90000001):
    st = load_paperdoll_state(char_id)
    app_row = MachoObject("carbon.common.script.sys.row.Row", {
        "header": ["hairDarkness"],
        "line": [float(st.get("hairDarkness", 0.27))],
    })
    colors_rs = MachoObject("eve.common.script.sys.rowset.Rowset", {
        "header": ["colorID", "colorNameA", "colorNameBC", "weight", "gloss"],
        "lines": st.get("colors", DEFAULT_PAPERDOLL_STATE["colors"]),
    })
    mods_rs = MachoObject("eve.common.script.sys.rowset.Rowset", {
        "header": ["modifierLocationID", "paperdollResourceID", "paperdollResourceVariation"],
        "lines": st.get("modifiers", DEFAULT_PAPERDOLL_STATE["modifiers"]),
    })
    sculpts_rs = MachoObject("eve.common.script.sys.rowset.Rowset", {
        "header": ["sculptLocationID", "weightUpDown", "weightLeftRight", "weightForwardBack"],
        "lines": st.get("sculpts", DEFAULT_PAPERDOLL_STATE["sculpts"]),
    })
    return MachoObject("utillib.KeyVal", {
        "appearance": app_row,
        "colors": colors_rs,
        "modifiers": mods_rs,
        "sculpts": sculpts_rs,
    })

def build_portrait_data_keyval(char_id=90000001):
    st = load_paperdoll_state(char_id)
    p = dict(DEFAULT_PAPERDOLL_STATE["portrait"])
    p.update(st.get("portrait", {}))
    p["__keys__"] = [k for k in p.keys() if k != "__keys__"]
    return MachoObject("utillib.KeyVal", p)