#!/bin/python

import traceback
from datetime import datetime, timedelta
import socket
from threading import Thread, Lock
from streamer_utils import SocketBlocksWrapper

import sqlite3
db_connection = sqlite3.connect("boards.sqlite3", check_same_thread=False)

LISTEN = ('', 40100)
MAX_ATTEMPTS = 3
BLOCK_TIME_MINUTES = 3

__log_lock = Lock()


def _log(message, owner="__base__"):
    with __log_lock:
        print(f"[{datetime.now().strftime('%H:%M:%S.%f')[:-3]}] {owner}: {message}")


class ServerWorker(Thread):
    def __init__(self, factory, conn, addr):
        super().__init__(daemon=True)
        self.conn = conn
        self._log_name = f"Worker-{addr}"
        self.factory = factory
        self._client_type = None
        self._client_name = None
        self._cursor = db_connection.cursor()

    def _auth_board(self, obj):
        if 'name' not in obj:
            return True, 'missing field "name"!'

        self._cursor.execute("SELECT board_name, password FROM users WHERE board_name = ?;", (obj['name'], ))
        res = self._cursor.fetchone()
        if res is None:
            return True, 'this board is not registered!'

        self._client_name = obj['name']
        self._client_type = "board"
        return False, None

    def _auth_client(self, obj):
        if 'target' not in obj:
            return True, 'missing field "target"!'
        if 'password' not in obj:
            return True, 'missing field "password"!'

        self._cursor.execute("SELECT board_name, password, blocked_until, wrong_attempts FROM users "
                             "WHERE board_name = ?;",
                             (obj['target'], ))
        res = self._cursor.fetchone()

        # сначала проверка на то, что плата существует
        if res is None:
            return True, 'Аккаунт не найден!'

        # теперь на то, что аккаунт не заблокирован
        now = datetime.now()
        delta = res[2] - int(round(now.timestamp()))
        print(delta)
        if delta >= 0:
            return True, f'Доступ отклонен: аккаунт заблокирован! Разблокировка через {delta} секунд'

        # проверка пароля
        if res[1] != obj['password']:
            if res[3] >= MAX_ATTEMPTS - 1:
                dt = now + timedelta(minutes=BLOCK_TIME_MINUTES)
                t = int(round(dt.timestamp()))
                self._cursor.execute("UPDATE users SET wrong_attempts = 0, blocked_until = ? WHERE board_name = ?;",
                                     (t, res[0]))
                db_connection.commit()
                return True, f'Доступ отклонен: аккаунт заблокирован! Разблокировка через {BLOCK_TIME_MINUTES} минут(ы)'
            else:
                self._cursor.execute("UPDATE users SET wrong_attempts = wrong_attempts + 1 WHERE board_name = ?;",
                                     (res[0], ))
                db_connection.commit()
                return True, 'Доступ отклонен: неверный пароль!'

        # обновление неудачных попыток в случае если пароль верный
        if res[3] != 0:
            self._cursor.execute("UPDATE users SET wrong_attempts = 0 WHERE board_name = ?;",
                                 (res[0], ))
            db_connection.commit()

        self._client_name = obj['target']
        self._client_type = "client"
        return False, None

    def _auth(self):
        _log("Wait for auth...", self._log_name)
        err = True

        obj = self.conn.read_object()
        if type(obj) != dict:
            description = "invalid object type!"
        elif 'type' not in obj:
            description = 'missing field "type"!'
        elif obj['type'] != 'auth':
            description = 'field "type" must have value "auth"!'
        elif 'client-type' not in obj:
            description = 'missing field "client-type"!'
        elif obj['client-type'] != 'board' and obj['client-type'] != 'client':
            description = f'unsupported client type: "{obj["client-type"]}"'
        else:
            if obj['client-type'] == 'board':
                err, description = self._auth_board(obj)
            else:
                err, description = self._auth_client(obj)

        response = {
            'type': 'auth-response'
        }

        if err:
            response["status"] = "failed"
            response["description"] = description
        else:
            response["status"] = "success"

        _log(f"auth {response['status']}! request={obj}, response={response}", owner=self._log_name)

        self.conn.write_object(response)
        return not err

    def run(self):
        try:
            with self.conn:
                if self._auth():
                    while True:
                        recv = self.conn.read_object()
                        if recv is None:
                            break
                        # _log(f"received {recv['type']} frame", self._log_name)
                        self.factory.route_packet(self, recv)

        except Exception:
            traceback.print_exc()

        finally:
            self.factory.remove_connection(self)

        _log(f"Close connection!", self._log_name)

    def get_name(self):
        return self._client_name

    def get_log_name(self):
        return self._log_name

    def send_packet(self, packet):
        self.conn.write_object(packet)
        # _log("send routed packet", self._log_name)


class ServerWorkerFactory:
    def __init__(self):
        self._lock = Lock()
        self._connections = []

    def add_connection(self, conn, addr):
        with self._lock:
            worker = ServerWorker(self, conn, addr)
            worker.start()
            self._connections.append(worker)

    def remove_connection(self, conn: ServerWorker):
        with self._lock:
            if conn in self._connections:
                _log(f"remove connection {conn.get_log_name()}", "ServerWorkerFactory")
                self._connections.remove(conn)

    def route_packet(self, owner: ServerWorker, data):
        connections = None
        with self._lock:
            if owner in self._connections:
                connections = self._connections.copy()

        if connections is not None:
            name = owner.get_name()
            for c in connections:
                if c == owner:
                    continue
                if c.get_name() == name:
                    c.send_packet(data)


def server_listener():
    _log("============ SERVER ============")
    _log("Creating table...")
    cur = db_connection.cursor()
    cur.execute("""CREATE TABLE IF NOT EXISTS users (
        board_name TEXT NOT NULL PRIMARY KEY,
        password TEXT DEFAULT '' NOT NULL,
        blocked_until INT DEFAULT 0 NOT NULL,
        wrong_attempts INT DEFAULT 0 NOT NULL
    );""")
    db_connection.commit()

    with socket.socket() as sock:
        sock.bind(LISTEN)
        _log(f"socket bind success", "ServerTask")
        sock.listen(8)

        _log(f"socket listen...", "ServerTask")
        worker = ServerWorkerFactory()

        while True:
            connection, addr = sock.accept()
            _log(f"connected from {addr}", "ServerTask")
            worker.add_connection(SocketBlocksWrapper(connection), addr)


if __name__ == '__main__':
    server_listener()



