import os
import sys
import time
import socket
import logging
from logging.handlers import RotatingFileHandler
import threading
import itertools

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, BASE_DIR)

from net import macho_marshal
from net.macho_marshal import MachoObject, long_type
from net.packets import (
    HANDSHAKE_CODE_BYTES, LOGIN_AUTH_FAILED_BYTES,
    placebo_crypto_hash, build_ping_rsp
)
from session import ClientSession
import db
from db import load_config, get_db_conn  # Re-exported for manage_users.py compatibility
import services

LOG_DIR = os.path.join(BASE_DIR, "logs")

def setup_logging():
    if not os.path.exists(LOG_DIR):
        os.makedirs(LOG_DIR)
    logger = logging.getLogger("dystropia")
    logger.setLevel(logging.INFO)
    if not logger.handlers:
        fmt = logging.Formatter("[%(asctime)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
        fh = RotatingFileHandler(
            os.path.join(LOG_DIR, "machonet_server.log"),
            maxBytes=5 * 1024 * 1024,
            backupCount=3
        )
        fh.setFormatter(fmt)
        sh = logging.StreamHandler(sys.stdout)
        sh.setFormatter(fmt)
        logger.addHandler(fh)
        logger.addHandler(sh)
    return logger

logger = setup_logging()

def _build_llve_7tuple(cfg):
    release_tag = "%s@%s" % (str(cfg["boot_codename"]), str(cfg["boot_region"]))
    return (
        int(cfg["handshake_secret"]),
        int(cfg["macho_version"]),
        int(cfg["user_count"]),
        float(cfg["boot_version"]),
        int(cfg["boot_build"]),
        release_tag,
        None,
    )

def handle_client(conn, addr, cfg):
    logger.info("[+] Connection accepted from %s:%d", addr[0], addr[1])
    conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
    conn.settimeout(300.0)

    session = ClientSession(conn, addr, cfg)
    llve_7tuple = _build_llve_7tuple(cfg)

    try:
        macho_marshal.send_packet(conn, macho_marshal.dumps(llve_7tuple))
        logger.info("[>] Sent Server LLVE 7-Tuple: %r", llve_7tuple)

        for step in itertools.count(1):
            pkt = macho_marshal.recv_packet(conn)
            if pkt is None:
                logger.info("[*] Client closed socket cleanly after step %d.", step - 1)
                break

            decoded = macho_marshal.loads(pkt)
            logger.debug("[<] Step %d - Received %d bytes from client: %r", step, len(pkt), decoded)

            # Step 1: Version Exchange & Auto-Heal
            if isinstance(decoded, tuple) and len(decoded) == 6 and isinstance(decoded[4], int) and isinstance(decoded[5], (str, bytes)):
                c_macho, c_ver, c_build, c_tag = int(decoded[1]), float(decoded[3]), int(decoded[4]), str(decoded[5])
                c_code = c_tag.split("@")[0] if "@" in c_tag else str(cfg["boot_codename"])
                if c_build != int(cfg["boot_build"]) or c_macho != int(cfg["macho_version"]) or c_ver != float(cfg["boot_version"]):
                    logger.info("[*] Step 1 Auto-Heal: Updating server build %s -> %s (version=%s, macho=%s, tag=%s)",
                                cfg["boot_build"], c_build, c_ver, c_macho, c_tag)
                    cfg["boot_build"] = c_build
                    cfg["boot_version"] = c_ver
                    cfg["macho_version"] = c_macho
                    cfg["boot_codename"] = c_code
                    db.save_config(cfg)
                llve_7tuple = _build_llve_7tuple(cfg)

            # Step 2: Queue Check ('QC')
            elif isinstance(decoded, tuple) and len(decoded) == 2 and decoded[1] == "QC":
                logger.info("[>] Replying to Queue Check ('QC') with queue position 0")
                macho_marshal.send_packet(conn, macho_marshal.dumps(0))
                logger.info("[>] Pre-sending fresh Server LLVE 7-Tuple for Connect reuse")
                macho_marshal.send_packet(conn, macho_marshal.dumps(llve_7tuple))

            # Step 3: VIP Key ('VK')
            elif isinstance(decoded, tuple) and len(decoded) == 3 and decoded[1] == "VK":
                logger.info("[*] Received VIP Key ('VK') for username hash: %r", decoded[2])

            # Step 4: Crypto Context ('placebo')
            elif isinstance(decoded, tuple) and len(decoded) == 2 and decoded[0] == "placebo":
                logger.info("[>] Received Client Crypto Context ('placebo') -> Sending 'OK CC'")
                macho_marshal.send_packet(conn, macho_marshal.dumps("OK CC"))

            # Step 5/6: Client Credentials & Challenge
            elif isinstance(decoded, tuple) and len(decoded) == 2 and isinstance(decoded[1], dict) and len(decoded[0]) == 64:
                client_challenge, req_dict = decoded
                raw_uname = req_dict.get("user_name", "gm_eric")
                pw_hash = req_dict.get("user_password_hash")
                logger.info("[*] Received Client Credentials for user_name=%r!", raw_uname)

                db_user = db.db_authenticate_user(cfg, raw_uname)
                if not db_user:
                    logger.warning("[!] Rejecting login at Step 6 for unknown user_name=%r!", raw_uname)
                    macho_marshal.send_packet(conn, LOGIN_AUTH_FAILED_BYTES)
                    time.sleep(0.25)
                    break

                session.user_id = int(db_user["user_id"])
                session.user_name = str(db_user["user_name"])
                session.role = long_type(db_user["role"])
                session.user_type = int(db_user["user_type"])
                session.pw_hash_step4 = pw_hash

                if pw_hash and not db.db_verify_or_set_password(cfg, session.user_id, pw_hash):
                    logger.warning("[!] Rejecting login at Step 6 for user_id=%d due to invalid password!", session.user_id)
                    macho_marshal.send_packet(conn, LOGIN_AUTH_FAILED_BYTES)
                    time.sleep(0.25)
                    break

                logger.info("[>] Sending passwordVersion = 2")
                macho_marshal.send_packet(conn, macho_marshal.dumps(2))

                if req_dict.get("boot_build") and int(req_dict["boot_build"]) != int(cfg["boot_build"]):
                    cfg["boot_build"] = int(req_dict["boot_build"])
                    cfg["boot_version"] = float(req_dict.get("boot_version", cfg["boot_version"]))
                    cfg["boot_codename"] = str(req_dict.get("boot_codename", cfg["boot_codename"]))
                    cfg["macho_version"] = int(req_dict.get("macho_version", cfg["macho_version"]))
                    db.save_config(cfg)

                server_challenge = b"\x00" * 64
                signed_func = (HANDSHAKE_CODE_BYTES, 0)
                challenge_hash = placebo_crypto_hash(client_challenge)
                resp_dict = {
                    "challenge_responsehash": challenge_hash,
                    "macho_version": int(cfg["macho_version"]),
                    "boot_version": float(cfg["boot_version"]),
                    "boot_build": int(cfg["boot_build"]),
                    "boot_codename": str(cfg["boot_codename"]),
                    "boot_region": str(cfg["boot_region"]),
                    "cluster_usercount": int(cfg["user_count"]),
                    "proxy_nodeid": 1,
                    "user_logonqueueposition": 1,
                    "config_vals": {"imageserverurl": "https://dystropia.com/images/"},
                }
                ch_tuple = (server_challenge, signed_func, {}, resp_dict)
                logger.info("[>] Sending Server Challenge 4-Tuple (challenge_responsehash=%s)", challenge_hash)
                macho_marshal.send_packet(conn, macho_marshal.dumps(ch_tuple))

            # Step 7: Final Challenge-Response Verification & Session Init ACK
            elif isinstance(decoded, tuple) and len(decoded) == 3 and decoded[0] == "55087":
                logger.info("[*] Received Valid Client Challenge-Response!")
                cand_hash = session.pw_hash_step4
                for item in (decoded[1], decoded[2]):
                    if isinstance(item, (bytes, str)) and item != "55087" and len(item) >= 16:
                        cand_hash = item
                        break
                if not db.db_verify_or_set_password(cfg, session.user_id, cand_hash):
                    logger.warning("[!] Rejecting login at Step 7 for user_id=%d due to invalid password!", session.user_id)
                    macho_marshal.send_packet(conn, LOGIN_AUTH_FAILED_BYTES)
                    time.sleep(0.25)
                    break

                pub_addr = str(cfg.get("public_address", "dystropia.com:26000"))
                ack_dict = {
                    "sessionID": session.user_id,
                    "user_clientid": session.client_id,
                    "session_init": {
                        "userid": session.user_id,
                        "role": session.role,
                        "userType": session.user_type,
                        "address": pub_addr,
                        "languageID": "EN",
                        "countryCode": "US",
                    },
                    "access_token": None,
                    "computer_hash": "dystropia_sandbox",
                    "client_hash": None,
                }
                logger.info("[>] Sending Final Server Challenge-Response ACK for user_id=%d!", session.user_id)
                macho_marshal.send_packet(conn, macho_marshal.dumps(ack_dict))

            # PingReq
            elif isinstance(decoded, MachoObject) and "PingReq" in decoded.class_name:
                ping_rsp = build_ping_rsp(decoded, session.client_id)
                macho_marshal.send_packet(conn, macho_marshal.dumps(ping_rsp))
                logger.info("[>] Sent PingRsp for PingReq")

            # CallReq
            elif isinstance(decoded, MachoObject) and "CallReq" in decoded.class_name:
                services.handle_call_req(session, decoded)

    except socket.timeout:
        logger.info("[*] Connection idle timeout (300s) for %s:%d.", addr[0], addr[1])
    except Exception as e:
        logger.exception("[!] Exception in client handler for %s:%d: %s", addr[0], addr[1], e)
    finally:
        conn.close()
        logger.info("[-] Connection closed for %s:%d", addr[0], addr[1])

def main():
    cfg = load_config()
    host = str(cfg["host"])
    port = int(cfg["port"])

    db.init_db_pool(cfg)

    srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind((host, port))
    srv.listen(16)

    print("=================================================================")
    print(" DYSTROPIA.COM MachoNet + PostgreSQL Server Online (Python %s)" % sys.version.split()[0])
    print(" Database  : PostgreSQL ThreadedPool (%s)" % cfg.get("db", {}).get("dbname", "ericgw_dystropia"))
    print(" Listening : %s:%d (Public: %s)" % (host, port, cfg.get("public_address", "dystropia.com:26000")))
    print(" Build     : %d (MachoVersion %d | %s@%s)" % (
        cfg["boot_build"], cfg["macho_version"], cfg["boot_codename"], cfg["boot_region"]
    ))
    print("=================================================================")
    sys.stdout.flush()

    while True:
        conn, addr = srv.accept()
        t = threading.Thread(target=handle_client, args=(conn, addr, cfg))
        t.daemon = True
        t.start()

if __name__ == "__main__":
    main()