import base64
import json
import os.path
import re
import time
import sys
from urllib.parse import urlparse
from dataclasses import dataclass, field
from typing import Tuple, Optional, List, Union, Dict

if "/www/server/panel/class" not in sys.path:
    sys.path.insert(0, "/www/server/panel/class")

import public
import db

if "/www/server/panel" not in sys.path:
    sys.path.insert(0, "/www/server/panel")


@dataclass
class NodeAPPKey:
    origin: str
    request_token: str
    app_key: str
    app_token: str

    def to_string(self)->str:
        data = "|".join((self.origin, self.request_token, self.app_key, self.app_token))
        return base64.b64encode(data.encode()).decode("utf-8")


@dataclass
class Node:
    remarks: str
    id: int = 0
    address: str = ""
    category_id: int = 0
    api_key: str = ""
    create_time: int = 0
    server_ip: str = ""
    status: int = 1
    error: dict = field(default_factory=dict)
    error_num: int = 0
    app_key: str = ""
    ssh_conf: dict = field(default_factory=dict)
    lpver: str = ""

    @classmethod
    def from_dict(cls, data: dict) -> Tuple[Optional["Node"], str]:
        if not isinstance(data.get('remarks', None), str):
            return None, 'remarks is required'
        if not data["remarks"].strip():
            return None, 'remarks is required'
        data["remarks"] = data["remarks"].strip()

        api_key = data.get('api_key', '')
        app_key = data.get('app_key', '')
        ssh_conf: dict = data.get('ssh_conf', {})
        if not api_key and not app_key and not ssh_conf:
            return None, 'api_key or app_key or ssh_conf is required'

        if app_key:
            app = cls.parse_app_key(app_key)
            if not app:
                return None, 'App_key format error'
            data["address"] = app.origin
            url = urlparse(data["address"], allow_fragments=False)
            if not url.scheme or not url.netloc:
                return None, 'address is invalid'

        if api_key:
            if not isinstance(data.get('address', None), str):
                return None, 'address is required'
            url = urlparse(data["address"], allow_fragments=False)
            if not url.scheme or not url.netloc:
                return None, 'address is invalid'

        if ssh_conf:
            for key in ("host", "port"):
                if key not in ssh_conf:
                    return None, 'ssh_conf is invalid'
            if "username" not in ssh_conf:
                ssh_conf["username"] = "root"
            if "password" not in ssh_conf:
                ssh_conf["password"] = ""
            if "pkey" not in ssh_conf:
                ssh_conf["pkey"] = ""
            if "pkey_passwd" not in ssh_conf:
                ssh_conf["pkey_passwd"] = ""

        if ssh_conf and not data.get("address", None):
            data["address"] = ssh_conf["host"]

        n = Node(
            data["remarks"], id=data.get('id', 0), address=data.get("address"), category_id=int(data.get('category_id', 0)),
            api_key=api_key, create_time=data.get('create_time', 0), server_ip=data.get('server_ip', ''),
            status=data.get('status', 1), error=data.get('error', {}), error_num=data.get('error_num', 0),
            app_key=app_key, ssh_conf=ssh_conf, lpver=data.get('lpver', '')
        )
        return n, ''

    def to_dict(self) -> dict:
        return {
            "remarks": self.remarks,
            "id": self.id,
            "address": self.address,
            "category_id": self.category_id,
            "api_key": self.api_key,
            "create_time": self.create_time,
            "server_ip": self.server_ip,
            "status": self.status,
            "error": self.error,
            "error_num": self.error_num,
            "app_key": self.app_key,
            "ssh_conf": self.ssh_conf,
            "lpver": self.lpver
        }

    def parse_server_ip(self):
        import socket
        from urllib.parse import urlparse
        if not self.address.startswith("http"):
            host = self.address  # 仅 ssh时 address本身就是host
        else:
            host = urlparse(self.address).hostname
        if isinstance(host, str) and public.check_ip(host):
            return host
        try:
            ip_address = socket.gethostbyname(host)
            return ip_address
        except socket.gaierror as e:
            public.print_log(f"Error: {e}")
            return ""

    @staticmethod
    def parse_app_key(app_key: str) -> Optional[NodeAPPKey]:
        try:
            data = base64.b64decode(app_key).decode("utf-8")
            origin, request_token, app_key, app_token = data.split("|")
            origin_arr = origin.split(":")
            if len(origin_arr) > 3:
                origin = ":".join(origin_arr[:3])
            return NodeAPPKey(origin, request_token, app_key, app_token)
        except:
            return None


class ServerNodeDB:
    _DB_FILE = public.get_panel_path() + "/data/db/node.db"
    _DB_INIT_FILE = os.path.dirname(__file__) + "/node.sql"

    def __init__(self):
        sql = db.Sql()
        sql._Sql__DB_FILE = self._DB_FILE
        self.db = sql

    def init_db(self):
        sql_data = public.readFile(self._DB_INIT_FILE)
        import sqlite3
        conn = sqlite3.connect(self._DB_FILE)
        cur = conn.cursor()
        cur.executescript(sql_data)
        cur.execute("PRAGMA table_info(node)")
        existing_cols = [row[1] for row in cur.fetchall()]
        if "ssh_test" in existing_cols:
            pass
            # print("字段 ssh_test 已存在")
        else:
            cur.execute("ALTER TABLE node ADD COLUMN ssh_test INTEGER DEFAULT (0)")
        conn.commit()
        conn.close()

    def close(self):
        self.db.close()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, exc_trackback):
        self.close()

    def __del__(self):
        self.close()

    def is_local_node(self, node_id: int):
        return self.db.table('node').where("id=? AND app_key = 'local' AND api_key = 'local'", (node_id,)).count() > 0

    def get_local_node(self):
        data = self.db.table('node').where("app_key = 'local' AND api_key = 'local'", ()).find()
        if isinstance(data, dict):
            return data
        return {
            "id": 0,
            "address": "",
            "category_id": 0,
            "remarks": "Local node",
            "api_key": "local",
            "create_time": time.strftime('%Y-%m-%d %H:%M:%S'),
            "server_ip": "127.0.0.1",
            "status": 0,
            "error": 0,
            "error_num": 0,
            "app_key": "local",
            "ssh_conf": "{}",
            "lpver": "",
        }

    def create_node(self, node: Node) -> str:
        node_data = node.to_dict()
        node_data.pop("id")
        node_data["create_time"] = time.strftime('%Y-%m-%d %H:%M:%S')
        node_data.pop("error")
        node_data["status"] = 1
        node_data["ssh_conf"] = json.dumps(node_data["ssh_conf"])

        if node.category_id > 0 and not self.category_exites(node.category_id):
            return "Classification does not exist"

        if self.db.table('node').where('remarks=?', (node.remarks,)).count() > 0:
            return "The node with this name already exists"
        try:
            node_id = self.db.table('node').insert(node_data)
            if isinstance(node_id, int):
                node.id = node_id
                return ""
            elif isinstance(node_id, str):
                return node_id
            else:
                return str(node_id)
        except Exception as e:
            return str(e)

    def update_node(self, node: Node, with_out_fields: List[str] = Node) -> str:
        if self.is_local_node(node.id):
            return "Cannot modify local nodes"
        if not self.node_id_exites(node.id):
            return "Node does not exist"
        node_data = node.to_dict()
        node_data.pop("create_time")
        node_data.pop("id")
        node_data["ssh_conf"] = json.dumps(node_data["ssh_conf"])
        node_data["error"] = json.dumps(node_data["error"])
        if with_out_fields and isinstance(with_out_fields, list):
            for f in with_out_fields:
                if f in node_data:
                    node_data.pop(f)

        if node.category_id > 0 and not self.category_exites(node.category_id):
            node.category_id = 0
            node_data["category_id"] = 0
        try:
            res = self.db.table('node').where('id=?', (node.id,)).update(node_data)
            if isinstance(res, str):
                return res
        except Exception as e:
            return str(e)

        return ""

    def set_node_ssh_conf(self, node_id: int, ssh_conf: dict, ssh_test: int=0):
        pdata = {"ssh_conf": json.dumps(ssh_conf)}
        if ssh_test:
            pdata["ssh_test"] = 1
        self.db.table('node').where('id=?', (node_id,)).update(pdata)
        return

    def remove_node_ssh_conf(self, node_id: int):
        self.db.table('node').where('id=?', (node_id,)).update({"ssh_conf": "{}"})
        return

    def delete_node(self, node_id: int) -> str:
        if self.is_local_node(node_id):
            return "Cannot delete local node"
        if not self.node_id_exites(node_id):
            return "Node does not exist"
        try:
            res = self.db.table('node').where('id=?', (node_id,)).delete()
            if isinstance(res, str):
                return res
        except Exception as e:
            return str(e)
        return ""

    def find_node(self, api_key:str = "", app_key: str = "") -> Optional[dict]:
        res =  self.db.table('node').where('api_key=?', (api_key, app_key)).find()
        if isinstance(res, dict):
            return res
        else:
            return None

    def get_node_list(self,
                      search: str = "",
                      category_id: int = -1,
                      offset: int = 0,
                      limit: int = 10) -> Tuple[List[Dict], str]:
        try:
            args = []
            query_str = ""
            if search:
                query_str += "remarks like ?"
                args.append('%{}%'.format(search))
            if category_id >= 0:
                if query_str:
                    query_str += " and category_id=?"
                else:
                    query_str += "category_id=?"
                args.append(category_id)
            if query_str:
                data_list = self.db.table('node').where(query_str, args).order('id desc').limit(limit, offset).select()
            else:
                data_list = self.db.table('node').order('id desc').limit(limit, offset).select()
            if self.db.ERR_INFO:
                return [], self.db.ERR_INFO
            if not isinstance(data_list, list):
                return [], str(data_list)
            return data_list, ""
        except Exception as e:
            return [], str(e)

    def query_node_list(self, *args) -> List[Dict]:
        return self.db.table('node').where(*args).select()

    def category_exites(self, category_id: int) -> bool:
        return self.db.table('category').where('id=?', (category_id,)).count() > 0

    def node_id_exites(self, node_id: int) -> bool:
        return self.db.table('node').where('id=?', (node_id,)).count() > 0

    def category_map(self) -> Dict:
        default_data = {0: "Default classification"}
        data_list = self.db.table('category').field('id,name').select()
        if isinstance(data_list, list):
            for data in data_list:
                default_data[data["id"]] = data["name"]
        return default_data

    def node_map(self) -> Dict:
        default_data = {}
        data_list = self.db.table('node').field('id,remarks').select()
        if isinstance(data_list, list):
            for data in data_list:
                default_data[data["id"]] = data["remarks"]
        return default_data

    def create_category(self, name: str) -> str:
        if self.db.table('category').where('name=?', (name,)).count() > 0:
            return "The classification for this name already exists"
        try:
            res = self.db.table('category').insert({"name": name, "create_time": time.strftime('%Y-%m-%d %H:%M:%S')})
            if isinstance(res, str):
                return res
        except Exception as e:
            return str(e)
        return ""

    def delete_category(self, category_id: int):
        self.db.table('node').where('category_id=?', (category_id,)).update({"category_id": 0})
        self.db.table('category').where('id=?', (category_id,)).delete()

    def bind_category_to_node(self, node_id: List[int], category_id: int) -> str:
        if not node_id:
            return "Node ID cannot be empty"
        if category_id > 0 and not self.category_exites(category_id):
            return "Classification does not exist"

        try:
            err = self.db.table('node').where(
                'id in ({})'.format(",".join(["?"]*len(node_id))), (*node_id,)
            ).update({"category_id": category_id})
            if isinstance(err, str):
                return err

        except Exception as e:
            return str(e)
        return ""

    def node_count(self, search, category_id) -> int:
        try:
            args = []
            query_str = ""
            if search:
                query_str += "remarks like ?"
                args.append('%{}%'.format(search))
            if category_id >= 0:
                if query_str:
                    query_str += " and category_id=?"
                else:
                    query_str += "category_id=?"
                args.append(category_id)
            if query_str:
                count = self.db.table('node').where(query_str, args).order('id desc').count()
            else:
                count = self.db.table('node').order('id desc').count()
            return count
        except:
            return 0

    def get_node_by_id(self, node_id: int) -> Optional[Dict]:
        try:
            data = self.db.table('node').where('id=?', (node_id,)).find()
            if self.db.ERR_INFO:
                return None
            if not isinstance(data, dict):
                return None
            return data
        except:
            return None

class ServerMonitorRepo:
    _REPO_DIR = public.get_panel_path() + "/data/mod_node_status_cache/"

    def __init__(self):
        if not os.path.exists(self._REPO_DIR):
            os.makedirs(self._REPO_DIR)

    def set_wait_reboot(self, server_ip: str, start: bool):
        wait_file = os.path.join(self._REPO_DIR, "wait_reboot_{}".format(server_ip))
        if start:
            return public.writeFile(wait_file, "wait_reboot")
        else:
            if os.path.exists(wait_file):
                os.remove(wait_file)

    def is_reboot_wait(self, server_ip: str):
        wait_file = os.path.join(self._REPO_DIR, "wait_reboot_{}".format(server_ip))
        # 重器待等待时间超过10分钟认为超时
        return os.path.exists(wait_file) and os.path.getmtime(wait_file) > time.time() - 610

    @staticmethod
    def get_local_server_status():
        from system import system
        return system().GetNetWork(None)

    def get_server_status(self, server_id: int) -> Optional[Dict]:
        cache_file = os.path.join(self._REPO_DIR, "server_{}.json".format(server_id))
        if not os.path.exists(cache_file):
            return None

        mtime = os.path.getmtime(cache_file)
        if time.time() - mtime > 60 * 5:
            os.remove(cache_file)
            return None
        try:
            data = public.readFile(cache_file)
            if isinstance(data, str):
                return json.loads(data)
        except:
            return None

    def save_server_status(self, server_id: int, data: Dict) -> str:
        cache_file = os.path.join(self._REPO_DIR, "server_{}.json".format(server_id))
        try:
            public.writeFile(cache_file, json.dumps(data))
            return ""
        except Exception as e:
            return str(e)

    def remove_cache(self, server_id: int):
        cache_file = os.path.join(self._REPO_DIR, "server_{}.json".format(server_id))
        if os.path.exists(cache_file):
            os.remove(cache_file)