import io
import logging
import traceback

import dglog

import socket
import time
from tempfile import TemporaryFile
from typing import Optional, Tuple

from smb.SMBConnection import SMBConnection
from smb.base import SharedFile, NotConnectedError
from smb.smb_structs import OperationFailure

from .config import SMBConfig, MasterNode, BackupNode, Node


class SMBClient:
    def __init__(self, path: str | None = None,
                 username: str | None = None,
                 password: str | None = None,
                 domain: str | None = 'group.s7',
                 cfg: SMBConfig | None = None,
                 logger: logging.Logger | dglog.Logger | None = None):
        self.path: Optional[str] = path
        self.username: Optional[str] = username
        self.password: Optional[str] = password
        self.domain: Optional[str] = domain

        self.remote_name: Optional[str] = None
        self.service_name: Optional[str] = None
        self.share_path: Optional[str] = None

        self.conn: Optional[SMBConnection] = None

        self.logger = logger

        self.cfg = cfg
        self._set_master_node_cfg()
        self.cfg.master_node.current = True

        self._prepare_connection_node(self.cfg.master_node)
        if self.cfg.backup_node is not None:
            self._prepare_connection_node(self.cfg.backup_node)

        self.connect()

    def set_path(self, path: str):
        self.path = path

    def set_credentials(self, username: str, password: str, domain='group.s7'):
        assert username, "Empty username isn't allowed"
        assert password, "Empty password isn't allowed"
        self.username = username
        self.password = password
        self.domain = domain

    def _set_master_node_cfg(self):
        if self.cfg is None:
            self.cfg = SMBConfig(
                master_node=MasterNode(
                    path=self.path,
                    username=self.username,
                    password=self.password,
                ),
                backup_node=None,
                reconnect_wait_time=5,
                reconnect_attempts=5,
            )

    @staticmethod
    def _prepare_connection_node(node_cfg: Node):
        path_arr = node_cfg.path.replace('\\', '/').split('/')[2:]

        node_cfg.host = path_arr[0]
        node_cfg.service_name = path_arr[1]
        node_cfg.share_path = '/'.join(path_arr[2:])

    def prepare_connection(self, node_cfg: Node = None):
        path_arr = self.path.replace('\\', '/').split('/')[2:]

        host = path_arr[0] if node_cfg is None else node_cfg.host
        service_name = path_arr[1] if node_cfg is None else node_cfg.service_name
        share_path = '/'.join(path_arr[2:]) if node_cfg is None else node_cfg.share_path

        self.remote_name = host
        self.service_name = service_name
        self.share_path = share_path

    def _switch_current_node_cfg(self, node_cfg: Node):
        if self.cfg.master_node is not None and self.cfg.backup_node is not None:
            if (self.cfg.master_node.current and isinstance(node_cfg, MasterNode)) or (self.cfg.backup_node.current and isinstance(node_cfg, BackupNode)):
                self.cfg.master_node.current = not self.cfg.master_node.current
                self.cfg.backup_node.current = not self.cfg.backup_node.current
        else:
            self.cfg.master_node.current = True

    def __connect_node(self, node_cfg: Node):
        """
        Connect to SMB node
        :return: connection status
        """
        try:
            assert node_cfg.username, 'No username provided'
            assert node_cfg.password, 'No password provided'

            self.logger.log_info(f"Trying to connect to {node_cfg.host}...")

            self.path = node_cfg.path
            self.prepare_connection(node_cfg)

            conn = SMBConnection(username=node_cfg.username,
                                 password=node_cfg.password,
                                 my_name=socket.gethostname(),
                                 remote_name=node_cfg.host,
                                 domain=node_cfg.domain,
                                 use_ntlm_v2=True,
                                 is_direct_tcp=True)

            assert conn.connect(socket.gethostbyname(node_cfg.host), 445)
            try:
                conn.listPath(node_cfg.service_name, "")
            except Exception as err:
                raise err
            self.conn = conn
            if self.cfg.master_node is not None:
                if self.cfg.backup_node is not None:
                    self._switch_current_node_cfg(node_cfg)
                else:
                    self.cfg.master_node.current = True
            return True
        except OperationFailure as err:
            self.logger.log_warn(err.message)
            return False
        except Exception as err:
            self.logger.log_err(err)
            return False

    def connect(self):
        """
        Connect to SMB
        :return: SMBConnection instance
        """
        def __connect(node_cfg: Node, attempt):
            if self.__connect_node(node_cfg):
                self.logger.log_info(f"Successfully connected to {node_cfg.host}/{node_cfg.service_name}:{node_cfg.username}")
                return True
            else:
                self.logger.log_warn(f"[{attempt + 1}/{self.cfg.reconnect_attempts}] Failed to connect to {node_cfg.host}/{node_cfg.service_name}:{node_cfg.username}. try reconnecting...")
                time.sleep(self.cfg.reconnect_wait_time)
                return False

        for try_ in range(self.cfg.reconnect_attempts):
            if __connect(self.cfg.master_node, try_):
                break
        else:
            if self.cfg.backup_node is not None:
                self.logger.log_warn(f"Failed to connect to {self.cfg.master_node.host}/{self.cfg.master_node.service_name}:{self.cfg.master_node.username} try connect to {self.cfg.backup_node.host}/{self.cfg.backup_node.service_name}:{self.cfg.backup_node.username}")
                for try_ in range(self.cfg.reconnect_attempts):
                    if __connect(self.cfg.backup_node, try_):
                        break
                else:
                    self.logger.log_err("Could not connect to both master and backup nodes. Giving up..")
                    raise NotConnectedError
            else:
                self.logger.log_err("Could not connect to master and backup nodes is not set. Giving up..")
                raise NotConnectedError

    def __check_connection_node(self, node_cfg: Node) -> bool:
        try:
            self.conn.listPath(node_cfg.service_name, "")
            return True
        except (NotConnectedError, OperationFailure) as err:
            self.logger.log_warn(f"Connection lost: {err}. Reconnecting...")
            self.conn = None
            self.cfg.master_node.current = False
            if self.cfg.backup_node is not None:
                self.cfg.backup_node.current = False
            self.connect()
            return self.conn is not None
        except Exception as err:
            self.logger.log_err(f"Unexpected error during connection check: {err}. Traceback: {traceback.format_exc()}")
            return False

    def check_connection(self):
        if self.cfg.master_node.current:
            return self.__check_connection_node(self.cfg.master_node)
        elif self.cfg.backup_node and self.cfg.backup_node.current:
            return self.__check_connection_node(self.cfg.backup_node)
        return False

    def force_switch_node(self):
        assert self.cfg.backup_node, "Backup node is not set. Could not switch"
        self.logger.log_info("Switching node")
        if self.cfg.master_node.current:
            if self.__connect_node(self.cfg.backup_node):
                self.logger.log_info(f"Successfully connected to {self.cfg.backup_node.host}/{self.cfg.backup_node.service_name}:{self.cfg.backup_node.username}")
                return True
            return False
        elif self.cfg.backup_node.current:
            if self.__connect_node(self.cfg.master_node):
                self.logger.log_info(f"Successfully connected to {self.cfg.master_node.host}/{self.cfg.master_node.service_name}:{self.cfg.master_node.username}")
                return True
            return False
        return False

    def ls(self, dir_path: str = "", filter: str = "*") -> list[SharedFile]:
        if self.check_connection():
            files_list = [x for x in self.conn.listPath(self.service_name, f'{self.share_path}/{dir_path}', pattern=filter) if x.filename not in ('.', '..')]
            return files_list
        else:
            raise Exception("not available connection")

    def lsf(self, dir_path: str = "", filter: str = "*") -> list[SharedFile]:
        """
        Get list of filenames from directory by mask with '.' and '..'
        :param dir_path:
        :param filter:
        :return:
        """
        if self.check_connection():
            files_list = self.conn.listPath(self.service_name, f'{self.share_path}/{dir_path}', pattern=filter)
            return files_list
        else:
            raise Exception("not available connection")

    def ls_names(self, dir_path: str = "", filter="*") -> list[str]:
        """
        Get list of filenames from directory by mask
        :param dir_path: Subdirectory
        :param filter: Filter
        :return: список имен файлов в указанной директории
        """
        try:
            return [file.filename for file in self.ls(dir_path, filter=filter)]
        except Exception as ex:
            raise Exception("not available connection", ex)

    def has_file(self, file_name: str, dir_path: str = "") -> bool:
        """
        Check if file exists in directory
        :param dir_path: Subdirectory
        :param file_name: Filename for check
        :return: bool
        """
        if not self.check_connection():
            self.connect()
        return file_name in self.ls(dir_path)

    def get_file(self, filename, path_to='.') -> bool:
        """
        Download file from SMB
        :param filename: Filename
        :param path_to: Path to store file in local system
        :return: True if file downloaded else False
        """
        tmp_file = self.get_tmp_file(filename)
        got_file = False
        if tmp_file:
            try:
                name = f'{path_to}/{filename}'
                with open(name, 'wb') as f:
                    f.write(tmp_file.read())
                got_file = True
            except Exception as ex:
                got_file = False
            finally:
                if tmp_file is not None:
                    tmp_file.close()
        return got_file

    def put_file(self, filename: str, path_from: str):
        """
        Upload file to SMB
        :param filename: Filename
        :param path_from: where file stored in local storage
        :return:
        """
        try:
            if not self.check_connection():
                self.connect()
            name = f'{path_from}/{filename}'
            with open(name, 'rb') as f:
                _ = self.conn.storeFile(self.service_name, f'{self.share_path}/{filename}', f)
        except Exception as err:
            self.logger.log_err(err)
            raise err

    def get_tmp_file(self, filename: str) -> TemporaryFile:
        """
        Download file from SMB to temporary file
        :param filename: filename
        :return: temporary file from SMB
        """
        try:
            if not self.check_connection():
                self.connect()
            file_data = TemporaryFile()
            self.conn.retrieveFile(self.service_name, f"{self.share_path}/{filename}", file_data)
            file_data.seek(0)
            return file_data
        except Exception as err:
            self.logger.log_err(err)
            raise err

    def put_tmp_file(self, filename: str, payload: io.BytesIO):
        """
        Upload file to SMB from temporary file
        :param filename: Filename to save in remote storage
        :param payload: temporary file to upload to SMB
        :return:
        """
        try:
            if not self.check_connection():
                self.connect()
            self.conn.storeFile(self.service_name, f"{self.share_path}/{filename}", payload)
            return True
        except Exception as err:
            self.logger.log_err(err)
            return False

    def remove_file(self, filename: str):
        """
        Remove file from SMB
        :param filename: File for remove
        :return:
        """
        try:
            if not self.check_connection():
                self.connect()
            self.conn.deleteFiles(self.service_name, f"{self.share_path}/{filename}")
            return True
        except Exception as err:
            self.logger.log_err(err)
            return False

    def download(self, filename, path_to='.') -> bool:
        return self.get_file(filename, path_to)

    def download_as_tmp(self, filename) -> Tuple[TemporaryFile, bool]:
        temp_file = self.get_tmp_file(filename)
        if temp_file:
            return temp_file, True
        return None, False

    def upload(self, filename, path_from):
        self.put_file(filename, path_from)

    def upload_tmp_file(self, filename, tmp_file):
        self.put_tmp_file(filename, tmp_file)

    def delete(self, filename):
        self.remove_file(filename)

    def get_list_files(self, subfolder='', filter='*') -> list[SharedFile]:
        return self.ls(subfolder, filter=filter)
