import os
import re
import sys
import time
import signal
import asyncio
import datetime
import subprocess
from typing import Set, Tuple, Optional, Iterable
from GNServer import App as _App, AsyncClient, GNRequest, GNResponse, Url, responses

def restart_as_root():
    if os.geteuid() != 0: # type: ignore
        os.execvp("sudo", ["sudo", sys.executable] + sys.argv)
restart_as_root()

def _kill_process_by_port(port: int):

    def _run(cmd: list[str]) -> Tuple[int, str, str]:
        try:
            p = subprocess.run(cmd, capture_output=True, text=True, check=False)
            return p.returncode, p.stdout.strip(), p.stderr.strip()
        except FileNotFoundError:
            return 127, "", f"{cmd[0]} not found"

    def pids_from_fuser(port: int, proto: str) -> Set[int]:
        # fuser понимает 59367/udp и 59367/tcp (оба стека)
        rc, out, _ = _run(["fuser", f"{port}/{proto}"])
        if rc != 0:
            return set()
        return {int(x) for x in re.findall(r"\b(\d+)\b", out)}

    def pids_from_lsof(port: int, proto: str) -> Set[int]:
        # lsof -ti UDP:59367  /  lsof -ti TCP:59367
        rc, out, _ = _run(["lsof", "-ti", f"{proto.upper()}:{port}"])
        if rc != 0 or not out:
            return set()
        return {int(x) for x in out.splitlines() if x.isdigit()}

    def pids_from_ss(port: int, proto: str) -> Set[int]:
        # ss -H -uapn 'sport = :59367'  (UDP)  /  ss -H -tapn ... (TCP)
        flag = "-uapn" if proto == "udp" else "-tapn"
        rc, out, _ = _run(["ss", "-H", flag, f"sport = :{port}"])
        if rc != 0 or not out:
            return set()
        pids = set()
        for line in out.splitlines():
            # ... users:(("python3",pid=1234,fd=55))
            for m in re.finditer(r"pid=(\d+)", line):
                pids.add(int(m.group(1)))
        return pids

    def find_pids(port: int, proto: str | None) -> Set[int]:
        protos: Iterable[str] = [proto] if proto in ("udp","tcp") else ("udp","tcp")
        found: Set[int] = set()
        for pr in protos:
            # Порядок: fuser -> ss -> lsof (достаточно любого)
            found |= pids_from_fuser(port, pr)
            found |= pids_from_ss(port, pr)
            found |= pids_from_lsof(port, pr)
        # не убивать себя
        found.discard(os.getpid())
        return found

    def kill_pids(pids: Set[int]) -> None:
        if not pids:
            return
        me = os.getpid()
        for sig in (signal.SIGTERM, signal.SIGKILL): # type: ignore
            still = set()
            for pid in pids:
                if pid == me:
                    continue
                try:
                    os.kill(pid, sig)
                except ProcessLookupError:
                    continue
                except PermissionError:
                    print(f"[WARN] No permission to signal {pid}")
                    still.add(pid)
                    continue
                still.add(pid)
            if not still:
                return
            # подождём чуть-чуть
            for _ in range(10):
                live = set()
                for pid in still:
                    try:
                        os.kill(pid, 0)
                        live.add(pid)
                    except ProcessLookupError:
                        pass
                still = live
                if not still:
                    return
                time.sleep(0.1)

    def wait_port_free(port: int, proto: str | None, timeout: float = 3.0) -> bool:
        t0 = time.time()
        while time.time() - t0 < timeout:
            if not find_pids(port, proto):
                return True
            time.sleep(0.1)
        return not find_pids(port, proto)

    for proto in ("udp", "tcp"):
        pids = find_pids(port, proto)
    

        print(f"Гашу процессы на порту {port}: {sorted(pids)}")
        kill_pids(pids)

        if wait_port_free(port, proto):
            print(f"Порт {port} освобождён.")
        else:
            print(f"[ERROR] Не удалось освободить порт {port}. Возможно, другой netns/служба перезапускает процесс.")



from KeyisBTools.cryptography.sign import s1

import socket

def is_port_in_use(port: int, proto: str = "tcp") -> bool:
    if proto == "tcp":
        sock_type = socket.SOCK_STREAM
    elif proto == "udp":
        sock_type = socket.SOCK_DGRAM
    else:
        raise ValueError("proto должен быть 'tcp' или 'udp'")
    
    s = socket.socket(socket.AF_INET, sock_type)
    try:
        s.bind(("0.0.0.0", port))
    except OSError:
        return True
    finally:
        s.close()
    return False


class AsyncLogWriter:
    def __init__(self, path: str, flush_interval: int = 3):
        self.file = open(path, "w+")
        self.flush_interval = flush_interval
        self._stop = False
        self._start = False

    async def _flusher(self):
        while not self._stop:
            await asyncio.sleep(self.flush_interval)
            self.file.flush()

    def write(self, msg: str):
        self.file.write(msg + "\n")

        if not self._start:
            asyncio.create_task(self._flusher())
            self._start = True

    def close(self):
        self._stop = True
        self.file.flush()
        self.file.close()


class App():
    def __init__(self):
        self._app = _App()

        self._servers_start_files = {}

        self._access_key: Optional[str] = None

        self._default_venv_path = None
        self._default_TLS_paths = None
        self._log_dir_path: Optional[str] = None

        self._client = AsyncClient()

        self.__add_routes()



    def setAccessKey(self, key: str):
        self._access_key = key

    def setVenvPath(self, venv_path: str):
        self._default_venv_path = venv_path

    def setTLSPaths(self, cert_path: str, key_path: str) -> None:
        self._default_TLS_paths = (cert_path, key_path)

    def setLogDir(self, dir_path: str) -> None:
        if os.path.exists(dir_path):
            self._log_dir_path = dir_path
        else:
            raise Exception('Путь не найден')

    def writeLog(self, domain: str, msg: str):
        print(domain, msg)
        file: Optional[AsyncLogWriter] = self._servers_start_files[domain].get('_log_file')
        if file is not None:
            file.write(msg)


    def addServerStartFile(self,
                           domain: str,
                           file_path: str,
                           port: Optional[int] = None,
                           start_when_run: bool = False,
                           venv_path: Optional[str] = None,
                           vm_host: bool = False,
                           cert_path: Optional[str] = None,
                           key_path: Optional[str] = None,
                           host: str = '0.0.0.0',
                           log_path: Optional[str] = None
                           ):
        
        if log_path is None and self._log_dir_path is not None:
            log_path = self._log_dir_path + f'/{domain}' + f'{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}.log'

        if log_path and not os.path.exists(log_path):
            os.makedirs(os.path.dirname(log_path), exist_ok=True)



        self._servers_start_files[domain] = {
            "domain": domain,
            "path": file_path,
            "port": port,
            "start_when_run": start_when_run,
            "venv_path": venv_path if venv_path is not None else self._default_venv_path,
            "vm_host": vm_host,
            "cert_path": cert_path if cert_path is not None else self._default_TLS_paths[0] if self._default_TLS_paths is not None else None,
            "key_path": key_path if key_path is not None else self._default_TLS_paths[1] if self._default_TLS_paths is not None else None,
            "host": host,
            "log_path": log_path
            }

        
        if log_path is not None:
            file = AsyncLogWriter(log_path)
            
            self._servers_start_files[domain]["_log_file"] = file



    async def startLikeRun(self):
        for server in self._servers_start_files:
            if self._servers_start_files[server]["start_when_run"]:
                asyncio.create_task(self.startServer(server))

    async def startServer(self, domain: str, timeout: float = 30):
        if domain in self._servers_start_files:


            res = await self.checkServerHealth(domain, timeout=1.0)
            if res[0]:
                raise ValueError(f"Server alredy running: {domain}")
            


            server = self._servers_start_files[domain]
            path = server["path"]
            port = server["port"]
            venv_path = server["venv_path"]
            vm_host = server['vm_host']
            host = server['host']
            
            cert_path = server['cert_path']
            key_path = server['key_path']
            
            if vm_host and (cert_path is None or key_path is None):
                raise Exception('Не указаны сертификаты')
            
            if not os.path.isfile(path):
                raise ValueError(f"Server start file not found: {path}")
            
            
            if is_port_in_use(port, 'udp'):
                _kill_process_by_port(port)
                await asyncio.sleep(1)


            out_ = None

            if path.endswith('.py'):
                if venv_path is not None:
                    if not os.path.isdir(venv_path):
                        raise ValueError(f"Virtual environment path not found: {venv_path}")
                    python_executable = os.path.join(venv_path, 'bin', 'python')
                    if not os.path.isfile(python_executable):
                        raise ValueError(f"Python executable not found in virtual environment: {python_executable}")
                else:
                    python_executable = sys.executable

                if not vm_host:
                    argv = []
                else:
                    argv = [
                        'gn:vm-host:start',
                        str(domain),
                        str(port),
                        str(cert_path),
                        str(key_path),
                        str(host)
                    ]

                self.writeLog(domain, 'Server starting...')
                proc = subprocess.Popen(
                    [python_executable, path, *argv],
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    text=True
                )

                save_to_out = True
                save_out = []

                async def log_stream(stream, prefix):
                    while True:
                        line = await asyncio.to_thread(stream.readline)
                        if not line:
                            break

                        if save_to_out:
                            save_out.append(line.strip())

                        self.writeLog(domain, f"{prefix}: {line.strip()}")

                asyncio.create_task(log_stream(proc.stdout, ""))
                asyncio.create_task(log_stream(proc.stderr, "ERR"))

                try:
                    returncode = await asyncio.to_thread(proc.wait, 3)
                    self.writeLog(domain, f"Process exited with code {returncode}")
                    save_out.clear()
                except subprocess.TimeoutExpired:
                    self.writeLog(domain, "Process timed out")
                    out_ = '\n'.join(save_out)

                save_to_out = False

                # try:
                #     returncode = proc.wait(timeout=3)
                #     out, err = proc.communicate()
                #     self.writeLog(domain, f'Server error: {returncode}')
                #     self.writeLog(domain, f'stdout: {out}')
                #     self.writeLog(domain, f'stderr: {err}')

                #     print("Процесс завершился с кодом:", returncode)
                #     print("STDOUT:", out)
                #     print("STDERR:", err)
                #     out_ = out + '\n\n' + err
                # except subprocess.TimeoutExpired:
                #     pass
                

            else:
                self.writeLog(domain, 'Server starting...')
                subprocess.Popen([path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            
            r =  await self.checkServerHealth(domain, timeout=timeout, out=out_)
            self.writeLog(domain, f'Server checkServerHealth status-> {r[0]}')
            return r
        else:
            self.writeLog(domain, f'No server start file found with domain: {domain}')
            raise ValueError(f"No server start file found with domain: {domain}")
    


    async def _send_message_to_server(self, domain: str, path: str, payload: Optional[dict] = None, timeout: float = 1.0, res: list = []) -> Optional[GNResponse]:
        port = self._servers_start_files[domain].get("port")
        if port is None:
            raise ValueError(f"No port specified for server: {domain}")

        if path.startswith('/'):
            path = path[1:]

        c = self._client.request(GNRequest('POST', Url(f'gn://127.0.0.1:{port}/!gn-vm-host/{path}'), payload=payload), reconnect_wait=2)
        

        print(f'send request timeout: {timeout} (port: {port})')

        try:
            result = await asyncio.wait_for(c, timeout=timeout)
        except asyncio.TimeoutError:
            result = None
        
        print(f'result -> {result}')
        
        res.append(result)

    async def checkServerHealth(self, domain: str, timeout: float = 3.0, interval=5, out: Optional[str] = None):
        loop = asyncio.get_event_loop()
        end = loop.time() + timeout
        res = []
        count = 0
        while loop.time() < end:
            loop.create_task(self._send_message_to_server(domain, '/ping', timeout=timeout - interval * count, res=res))
            count+=1

            await asyncio.sleep(0.05)

            if res != []:
                return (True, out, res[0])
            else:
                await asyncio.sleep(interval)
            
        return (False, out, None)
        


    def stopServer(self, domain: str):
        if domain in self._servers_start_files:
            server = self._servers_start_files[domain]
            port = server["port"]
            if port is not None:
                _kill_process_by_port(port)
            else:
                raise ValueError(f"No port specified for server: {domain}")
        else:
            raise ValueError(f"No server start file found with domain: {domain}")

    async def reloadServer(self, domain: str, timeout: float = 1):
        if domain in self._servers_start_files:
            self.stopServer(domain)
            await asyncio.sleep(timeout)
            return await self.startServer(domain)
        else:
            raise ValueError(f"No server start file found with domain: {domain}")

    def run(self,
            domain,
            port,
            cert_path: str,
            key_path: str,
            *,
            host: str = '0.0.0.0',
            idle_timeout: float = 20.0,
            wait: bool = True
            ):
        

        self._app.run(
            domain=domain,
            port=port,
            cert_path=cert_path,
            key_path=key_path,

            host=host,
            idle_timeout=idle_timeout,
            wait=wait,
            run=self.startLikeRun,
        )


    def __resolve_access_key(self, request: GNRequest) -> bool:
        if self._access_key is None:
            raise ValueError("Access key is not set.")
        
        sign = request.cookies.get('vm-host-sign')

        if sign is None:
            return False

        return s1.verify(self._access_key.encode(), sign, 60)

    def __add_routes(self):
        @self._app.route('POST', '/ping')
        async def ping_handler(request: GNRequest, domain: Optional[str] = None, timeout: float = 3.0):
            if not self.__resolve_access_key(request):
                return None
            
            if not domain:
                return GNResponse('ok', {'time': datetime.datetime.now(datetime.timezone.utc).isoformat()})
            else:
                try:
                    result = await self.checkServerHealth(domain, timeout=timeout)
                    if result[0]:
                        return GNResponse('ok', {'message': f'Server {domain} is alive.', 'time': result[2].payload.get('time')})
                    else:
                        return GNResponse('error', {'error': f'Server {domain} is not responding.'})
                except ValueError as e:
                    return GNResponse('error', {'error': str(e)})
                
        @self._app.route('GET', '/servers')
        async def list_servers_handler(request: GNRequest):
            if not self.__resolve_access_key(request):
                return None
            
            servers_info = []
            for server in self._servers_start_files.values():
                servers_info.append(server['domain'])
            return GNResponse('ok', {'servers': servers_info})



            

        @self._app.route('POST', '/start-server')
        async def start_server_handler(request: GNRequest, domain: str = ''):
            if not self.__resolve_access_key(request):
                return None
            
            if not domain:
                return GNResponse('error', {'error': 'Server domain is required.'})
            try:
                result = await self.startServer(domain)
                if result[0]:
                    return GNResponse('ok', {'message': f'Server {domain} started.'})
                else:
                    return GNResponse('error', {'error': f'Server {domain} failed to start within the timeout period. {result[1]}'})
            except ValueError as e:
                return GNResponse('error', {'error': str(e)})
            
            

        @self._app.route('POST', '/reload-server')
        async def reload_server_handler(request: GNRequest, domain: str = '', timeout: float = 0.5):
            if not self.__resolve_access_key(request):
                return None

            if not domain:
                return GNResponse('error', {'error': 'Server domain is required.'})

            try:
                result = await self.reloadServer(domain, timeout)
                if result[0]:
                    return GNResponse('ok', {'message': f'Server {domain} reloaded.'})
                else:
                    return GNResponse('error', {'error': f'Server {domain} failed to reload within the timeout period. {result[1]}'})
            except ValueError as e:
                return GNResponse('error', {'error': str(e)})
        
        @self._app.route('POST', '/stop-server')
        async def stop_server_handler(request: GNRequest, domain: str = ''):
            if not self.__resolve_access_key(request):
                return None

            if not domain:
                return GNResponse('error', {'error': 'Server domain is required.'})

            try:
                self.stopServer(domain)
                return GNResponse('ok', {'message': f'Server {domain} stopped.'})
            except ValueError as e:
                return GNResponse('error', {'error': str(e)})
        
        @self._app.route('POST', '/start-all-servers')
        async def start_all_servers_handler(request: GNRequest):
            if not self.__resolve_access_key(request):
                return None

            for server in self._servers_start_files:
                try:
                    result = await self.startServer(server)
                    if not result:
                        return GNResponse('error', {'error': f'Server {server} failed to start within the timeout period.'})
                except ValueError as e:
                    return GNResponse('error', {'error': str(e)})
        
        @self._app.route('POST', '/stop-all-servers')
        async def stop_all_servers_handler(request: GNRequest):
            if not self.__resolve_access_key(request):
                return None

            for server in self._servers_start_files:
                try:
                    self.stopServer(server)
                except ValueError as e:
                    return GNResponse('error', {'error': str(e)})

            return GNResponse('ok', {'message': 'All servers stopped.'})
        
        @self._app.route('POST', '/reload-all-servers')
        async def reload_all_servers_handler(request: GNRequest, timeout: float = 0.5):
            if not self.__resolve_access_key(request):
                return None

            for server in self._servers_start_files:
                try:
                    result = await self.reloadServer(server, timeout)
                    if not result:
                        return GNResponse('error', {'error': f'Server {server} failed to reload within the timeout period.'})
                except ValueError as e:
                    return GNResponse('error', {'error': str(e)})
