import json
import math
import os
import re
import socket
import subprocess
import sys
import typing  # noqa

from dataclasses import dataclass
from urllib import request, parse
from urllib.error import HTTPError
from typing import Union

sys.path.append("a2s")
import a2s  # noqa
from a2s import dayzquery  # noqa

params = [
    r"\nor\1\map\chernarusplus\nor\1\map\sakhal\nor\1\map\enoch\empty\1\nor\1\map\namalsk",  # noqa
    r"\map\namalsk\empty\1",
    r"\map\namalsk\noplayers\1",
    r"\map\chernarusplus\empty\1",
    r"\map\chernarusplus\noplayers\1",
    r"\map\sakhal\empty\1",
    r"\map\sakhal\noplayers\1",
    r"\map\enoch\empty\1",
    r"\map\enoch\noplayers\1",
]


class BmAPIError(Exception):
    pass


class BmIdError(Exception):
    pass


class InvalidIpError(Exception):
    pass


def get_netmask() -> str:
    hostname = os.uname()[1]
    i = socket.gethostbyname(hostname)
    netmask = i.rsplit(".", 1)[0]
    return netmask


def test_ip(suffix: int, port: int) -> dict | None:
    netmask = get_netmask()
    hostname = f"{netmask}.{str(suffix)}"
    ping = ["ping", "-c1", "-i", "0.1", "-w", "1"]
    output = subprocess.run(ping + [hostname], capture_output=True)
    if output.returncode == 0:
        return query_direct(hostname, port)
    return None


def sanitize(name: str) -> str:
    name = re.sub(r"\r", r"", name)
    name = re.sub(r"\n", r"", name)
    name = re.sub(r"\x01", r"", name)
    name = re.sub(r"\ufeff", r"", name)
    name = re.sub(r"(^!\s*)", r"-", name)
    name = re.sub(r"(^-\s*)", r"", name)
    name = re.sub(r"(^-)", r"", name)
    name = re.sub(r"(^\s*)", r"", name)
    name = re.sub(r"\t", "", name)
    return name


def parse_json(json: list) -> list:
    """
    Server metadata is underspecified and server operators
    tend to insert random garbage in the headers. In case
    sanitization failed, discard malformed rows rather than
    aborting outright.
    """
    rows = []
    for row in json:
        try:
            name = sanitize(row["name"])
            if name == "":
                continue
        except KeyError:
            continue

        for key in [
            "map",
            "gametype",
            "players",
            "max_players",
            "addr",
            "gameport",
        ]:
            try:
                row[key]
            except KeyError:
                continue

        try:
            r = row["gametype"].split(",")
        except KeyError:
            continue

        if "no3rd" in r:
            view = "1PP"
        else:
            view = "3PP"

        if "external" in r:
            provider = "Unoffic."
        else:
            provider = "Official"

        if "mod" in r:
            modded = True
        else:
            modded = False

        try:
            r = row["gametype"].split("lqs")
            queue = r[1].split(",")[0]
        except IndexError:
            queue = 0

        try:
            test_time = re.search(r"[0-9]{2}:[0-9]{2}", row["gametype"])
            if test_time:
                time = test_time.group(0)
            else:
                time = "Unknown"
        except AttributeError:
            time = "Unknown"

        try:
            ip = row["addr"].split(":")[0] + ":" + str(row["gameport"])
            qport = row["addr"].split(":")[1]
        except IndexError:
            continue

        try:
            ping = row["ping"]
        except KeyError:
            ping = 9999

        mapname = row["map"].lower()
        players = row["players"]
        max_players = row["max_players"]
        raw = [
            name,
            mapname,
            view,
            time,
            int(players),
            int(max_players),
            int(queue),
            ip,
            int(qport),
            ping,
            provider,
            modded,
        ]
        rows.append(raw)
    return rows


def query_direct(ip: str, qport: int, TIMEOUT=3.0) -> dict | None:
    try:
        info = a2s.info((ip, qport), TIMEOUT)

        name = info.server_name
        mapname = info.map_name
        address = ip + ":" + str(qport)
        gameport = str(info.port)
        players = info.player_count
        max_players = info.max_players
        keywords = info.keywords

        try:
            ping = info.ping
            ping = math.floor(info.ping * 1000)
        except AttributeError:
            ping = 9999

        res = {}
        res["name"] = name
        res["map"] = mapname
        res["gametype"] = keywords
        res["players"] = players
        res["max_players"] = max_players
        res["addr"] = address
        res["gameport"] = gameport
        res["ping"] = ping
        return res
    except TimeoutError:
        return None
    except KeyError:
        return None


@dataclass(slots=True, frozen=True)
class Res:
    status: int
    parsed: bool
    json: Union[str, None]


@dataclass(slots=True, frozen=True)
class Ping:
    addr: str
    iteration: int
    ping: int


@dataclass(slots=True, frozen=True)
class Details:
    data: Union[list, None]
    description: str
    success: bool


@dataclass(slots=True, frozen=True)
class Prereqs:
    password: bool
    gameport: int
    appid: Union[int, None]
    version: Union[str, None]


@dataclass(slots=True)
class Record:
    """
    The gameport field is manipulated by the RowType.CONN_BY_IP method
    """
    ip: str
    gameport: int
    qport: int


def get_prereqs(ip: str, qport: int) -> Prereqs:
    try:
        info = a2s.info((ip, qport))
    except TimeoutError:
        return Prereqs(False, 0, None, None)

    gameport = getattr(info, "port", 0)
    is_password = getattr(info, "password_protected", False)
    appid = getattr(info, "game_id", None)
    version = getattr(info, "version", None)

    return Prereqs(is_password, gameport, appid, version)


def details(ip: str, qport: int) -> Details:
    default_str = "None provided"

    try:
        info = a2s.info((ip, qport))
    except TimeoutError:
        return Details(None, default_str, False)
    try:
        rules = dayzquery.dayz_rules((ip, int(qport)))
    except TimeoutError:
        return Details(None, default_str, False)

    try:
        keywords = info.keywords.split(",")
    except AttributeError:
        return Details(None, default_str, False)

    battleye = "Disabled"
    if "battleye" in keywords:
        battleye = "Enabled"

    day_accel = 0.0
    night_accel = 0.0
    for keyword in keywords:
        if "etm" in keyword:
            day_accel = float(keyword.lstrip("etm"))
            day_accel = float(f"{day_accel:g}")
        if "entm" in keywords:
            night_accel = float(keyword.lstrip("entm"))
            night_accel = float(f"{night_accel:g}")

    try:
        password = info.password_protected
        if password is False:
            password = "Disabled"
        else:
            password = "Enabled"
    except AttributeError:
        password = "-"

    try:
        vac = info.vac_enabled
        if vac is False:
            vac = "Disabled"
        else:
            vac = "Enabled"
    except AttributeError:
        vac = "-"

    try:
        version = info.version
    except AttributeError:
        version = "-"

    try:
        dlc = rules.dlc_flags
        if dlc == 0:
            dlc = "None"
        if dlc == 2:
            dlc = "Frostline"
    except AttributeError:
        dlc = "not specified"

    try:
        platform = rules.platform
        if platform == "win":
            platform = "Windows"
        if platform == "?":
            platform = "Linux"
    except AttributeError:
        platform = "not specified"

    try:
        description = rules.description.strip()
        if description == "":
            description = default_str
    except AttributeError:
        description = default_str

    rows = [
        ["Battleye", battleye],
        ["Daytime acceleration", f"{day_accel}x"],
        ["DLC", dlc],
        ["Night-time acceleration", f"{night_accel}x"],
        ["Password", password],
        ["Platform", platform],
        ["Valve Anti-Cheat", vac],
        ["Version", version],
    ]

    return Details(rows, description, True)


def ping(iteration: int, row: list) -> Ping:
    addr = row[7]
    qport = row[8]

    res = None

    if row[9] != 9999:
        return Ping(addr, iteration, row[9])

    try:
        ip = addr.split(":")[0]
    except IndexError:
        ping = 9999

    try:
        res = query_direct(ip, qport, 0.5)
    except Exception:
        pass

    if res is None:
        ping = 9999
    else:
        ping = res["ping"]

    return Ping(addr, iteration, ping)


def query_api(key: str, appid: int, param: str) -> Res:
    LIMIT = 10000
    url = "https://api.steampowered.com/IGameServersService/GetServerList/v1/?"

    payload: dict[str, Union[int, str]] = {
        "filter": r"\appid" + fr"\{appid}" + param,
        "limit": LIMIT,
        "key": key,
    }
    par = parse.urlencode(payload)
    url = f"{url}{par}"

    status = 200
    parsed = True
    data = None

    try:
        with request.urlopen(url) as response:
            if response.status != 200:
                status = response.status
            try:
                parsed = True
                data = json.load(response)
            except json.decoder.JSONDecodeError:
                parsed = False
                data = None
    except HTTPError:
        status = 403
        parsed = False
        data = None
    finally:
        return Res(status, parsed, data)


def query_bm_api(api_key: str, bm_id: str) -> Record:
    if bm_id.isnumeric() is False:
        raise BmIdError("ID must be numeric only")

    payload: dict[str, Union[int, str]] = {
        "sort": "-players",
        "filter[game]": "dayz",
        "filter[ids][whitelist]": bm_id,
    }

    url = "https://api.battlemetrics.com/servers?"
    par = parse.urlencode(payload)
    url = f"{url}{par}"

    hdr = {"Authorization": "Bearer " + api_key}
    r = request.Request(url, headers=hdr)

    try:
        with request.urlopen(r) as response:
            try:
                j = json.load(response)
            except json.decoder.JSONDecodeError:
                raise BmAPIError("Malformed response from Battlemetrics")

            if len(j["data"]) < 1:
                raise BmAPIError("Not a valid Battlemetrics ID")
            j = j["data"][0]["attributes"]
            return Record(j["ip"], j["port"], j["portQuery"])
    except HTTPError:
        raise BmAPIError("Failed to query Battlemetrics")


def validate_ip(addr: str):
    fields = addr.split(":")
    if len(fields) != 2:
        raise InvalidIpError("Address must be formatted as IP:Queryport")

    ip = fields[0]
    port = fields[1]

    try:
        int(port)
    except ValueError:
        raise InvalidIpError(f"'{port}' is not a valid port")

    if int(port) > 65535 or int(port) < 0:
        raise InvalidIpError(f"'{port}' is not a valid port")

    try:
        socket.inet_aton(ip)
    except OSError:
        raise InvalidIpError(f"'{ip}' is not a valid IP")

    ip = addr.split(":")[0]
    qport = int(addr.split(":")[1])
    record = Record(ip, 0, qport)
    return record
