"""
Utility functions for SMB connections

:depends: impacket
"""

import logging
import socket
import uuid

import salt.utils.files
import salt.utils.stringutils
import salt.utils.versions
from salt.exceptions import MissingSmb

log = logging.getLogger(__name__)


try:
    from smbprotocol.connection import Connection
    from smbprotocol.create_contexts import (
        CreateContextName,
        SMB2CreateContextRequest,
        SMB2CreateQueryMaximalAccessRequest,
    )
    from smbprotocol.open import (
        CreateDisposition,
        CreateOptions,
        DirectoryAccessMask,
        FileAttributes,
        FileInformationClass,
        FilePipePrinterAccessMask,
        ImpersonationLevel,
        Open,
        ShareAccess,
    )
    from smbprotocol.security_descriptor import (
        AccessAllowedAce,
        AccessMask,
        AclPacket,
        SDControl,
        SIDPacket,
        SMB2CreateSDBuffer,
    )
    from smbprotocol.session import Session
    from smbprotocol.tree import TreeConnect

    logging.getLogger("smbprotocol").setLevel(logging.WARNING)
    HAS_SMBPROTOCOL = True
except ImportError:
    HAS_SMBPROTOCOL = False


class SMBProto:
    def __init__(self, server, username, password, port=445):
        connection_id = uuid.uuid4()
        addr = socket.getaddrinfo(server, None, 0, 0, socket.IPPROTO_TCP)[0][4][0]
        self.server = server
        connection = Connection(connection_id, addr, port, require_signing=True)
        self.session = Session(connection, username, password, require_encryption=False)

    def connect(self):
        self.connection.connect()
        self.session.connect()

    def close(self):
        self.session.connection.disconnect(True)

    @property
    def connection(self):
        return self.session.connection

    def tree_connect(self, share):
        if share.endswith("$"):
            share = rf"\\{self.server}\{share}"
        tree = TreeConnect(self.session, share)
        tree.connect()
        return tree

    @staticmethod
    def normalize_filename(file):
        return file.lstrip("\\")

    @classmethod
    def open_file(cls, tree, file):
        file = cls.normalize_filename(file)
        # ensure file is created, get maximal access, and set everybody read access
        max_req = SMB2CreateContextRequest()
        max_req["buffer_name"] = (
            CreateContextName.SMB2_CREATE_QUERY_MAXIMAL_ACCESS_REQUEST
        )
        max_req["buffer_data"] = SMB2CreateQueryMaximalAccessRequest()

        # create security buffer that sets the ACL for everyone to have read access
        everyone_sid = SIDPacket()
        everyone_sid.from_string("S-1-1-0")
        ace = AccessAllowedAce()
        ace["mask"] = AccessMask.GENERIC_ALL
        ace["sid"] = everyone_sid
        acl = AclPacket()
        acl["aces"] = [ace]
        sec_desc = SMB2CreateSDBuffer()
        sec_desc["control"].set_flag(SDControl.SELF_RELATIVE)
        sec_desc.set_dacl(acl)
        sd_buffer = SMB2CreateContextRequest()
        sd_buffer["buffer_name"] = CreateContextName.SMB2_CREATE_SD_BUFFER
        sd_buffer["buffer_data"] = sec_desc

        create_contexts = [max_req, sd_buffer]
        file_open = Open(tree, file)
        open_info = file_open.create(
            ImpersonationLevel.Impersonation,
            FilePipePrinterAccessMask.GENERIC_READ
            | FilePipePrinterAccessMask.GENERIC_WRITE,
            FileAttributes.FILE_ATTRIBUTE_NORMAL,
            ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE,
            CreateDisposition.FILE_OVERWRITE_IF,
            CreateOptions.FILE_NON_DIRECTORY_FILE,
        )
        return file_open

    @staticmethod
    def open_directory(tree, name, create=False):
        # ensure directory is created
        dir_open = Open(tree, name)
        if create:
            dir_open.create(
                ImpersonationLevel.Impersonation,
                DirectoryAccessMask.GENERIC_READ | DirectoryAccessMask.GENERIC_WRITE,
                FileAttributes.FILE_ATTRIBUTE_DIRECTORY,
                ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE,
                CreateDisposition.FILE_OPEN_IF,
                CreateOptions.FILE_DIRECTORY_FILE,
            )
        return dir_open


def _get_conn_smbprotocol(host="", username="", password="", client_name="", port=445):
    conn = SMBProto(host, username, password, port)
    conn.connect()
    return conn


def get_conn(host="", username=None, password=None, port=445):
    """
    Get an SMB connection
    """
    if HAS_SMBPROTOCOL:
        log.info("Get connection smbprotocol")
        return _get_conn_smbprotocol(host, username, password, port=port)
    else:
        return False


def _mkdirs_smbprotocol(
    path, share="C$", conn=None, host=None, username=None, password=None
):
    if conn is None:
        conn = get_conn(host, username, password)

    if conn is False:
        return False

    tree = conn.tree_connect(share)
    comps = path.split("/")
    pos = 1
    for comp in comps:
        cwd = "\\".join(comps[0:pos])
        dir_open = conn.open_directory(tree, cwd, create=True)
        compound_messages = [
            dir_open.query_directory(
                "*", FileInformationClass.FILE_NAMES_INFORMATION, send=False
            ),
            dir_open.close(False, send=False),
        ]
        requests = conn.session.connection.send_compound(
            [x[0] for x in compound_messages],
            conn.session.session_id,
            tree.tree_connect_id,
        )
        for i, request in enumerate(requests):
            response = compound_messages[i][1](request)
        pos += 1


def mkdirs(path, share="C$", conn=None, host=None, username=None, password=None):
    if HAS_SMBPROTOCOL:
        return _mkdirs_smbprotocol(
            path, share, conn=conn, host=host, username=username, password=password
        )
    raise MissingSmb("SMB library required (impacket or smbprotocol)")


def _put_str_smbprotocol(
    content, path, share="C$", conn=None, host=None, username=None, password=None
):
    if conn is None:
        conn = get_conn(host, username, password)
    if conn is False:
        return False
    tree = conn.tree_connect(share)
    try:
        file_open = conn.open_file(tree, path)
        file_open.write(salt.utils.stringutils.to_bytes(content), 0)
    finally:
        file_open.close()


def put_str(
    content, path, share="C$", conn=None, host=None, username=None, password=None
):
    """
    Wrapper around impacket.smbconnection.putFile() that allows a string to be
    uploaded, without first writing it as a local file
    """
    if HAS_SMBPROTOCOL:
        return _put_str_smbprotocol(
            content,
            path,
            share,
            conn=conn,
            host=host,
            username=username,
            password=password,
        )
    raise MissingSmb("SMB library required (impacket or smbprotocol)")


def _put_file_smbprotocol(
    local_path,
    path,
    share="C$",
    conn=None,
    host=None,
    username=None,
    password=None,
    chunk_size=1024 * 1024,
):
    if conn is None:
        conn = get_conn(host, username, password)
    if conn is False:
        return False

    tree = conn.tree_connect(share)
    file_open = conn.open_file(tree, path)
    with salt.utils.files.fopen(local_path, "rb") as fh_:
        try:
            position = 0
            while True:
                chunk = fh_.read(chunk_size)
                if not chunk:
                    break
                file_open.write(chunk, position)
                position += len(chunk)
        finally:
            file_open.close(False)


def put_file(
    local_path, path, share="C$", conn=None, host=None, username=None, password=None
):
    """
    Wrapper around impacket.smbconnection.putFile() that allows a file to be
    uploaded

    Example usage:

        import salt.utils.smb
        smb_conn = salt.utils.smb.get_conn('10.0.0.45', 'vagrant', 'vagrant')
        salt.utils.smb.put_file('/root/test.pdf', 'temp\\myfiles\\test1.pdf', conn=smb_conn)
    """
    if HAS_SMBPROTOCOL:
        return _put_file_smbprotocol(
            local_path,
            path,
            share,
            conn=conn,
            host=host,
            username=username,
            password=password,
        )
    raise MissingSmb("SMB library required (impacket or smbprotocol)")


def _delete_file_smbprotocol(
    path, share="C$", conn=None, host=None, username=None, password=None
):
    if conn is None:
        conn = get_conn(host, username, password)
    if conn is False:
        return False
    tree = conn.tree_connect(share)
    file_open = Open(tree, path)
    delete_msgs = [
        file_open.create(
            ImpersonationLevel.Impersonation,
            FilePipePrinterAccessMask.GENERIC_READ | FilePipePrinterAccessMask.DELETE,
            FileAttributes.FILE_ATTRIBUTE_NORMAL,
            ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE,
            CreateDisposition.FILE_OPEN,
            CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_DELETE_ON_CLOSE,
            send=False,
        ),
        file_open.close(False, send=False),
    ]
    requests = conn.connection.send_compound(
        [x[0] for x in delete_msgs],
        conn.session.session_id,
        tree.tree_connect_id,
        related=True,
    )
    responses = []
    for i, request in enumerate(requests):
        # A SMBResponseException will be raised if something went wrong
        response = delete_msgs[i][1](request)
        responses.append(response)


def delete_file(path, share="C$", conn=None, host=None, username=None, password=None):
    if HAS_SMBPROTOCOL:
        return _delete_file_smbprotocol(
            path, share, conn=conn, host=host, username=username, password=password
        )
    raise MissingSmb("SMB library required (impacket or smbprotocol)")


def _delete_directory_smbprotocol(
    path, share="C$", conn=None, host=None, username=None, password=None
):
    if conn is None:
        conn = get_conn(host, username, password)
    if conn is False:
        return False
    log.debug("_delete_directory_smbprotocol - share: %s, path: %s", share, path)
    tree = conn.tree_connect(share)

    dir_open = Open(tree, path)
    delete_msgs = [
        dir_open.create(
            ImpersonationLevel.Impersonation,
            DirectoryAccessMask.DELETE,
            FileAttributes.FILE_ATTRIBUTE_DIRECTORY,
            0,
            CreateDisposition.FILE_OPEN,
            CreateOptions.FILE_DIRECTORY_FILE | CreateOptions.FILE_DELETE_ON_CLOSE,
            send=False,
        ),
        dir_open.close(False, send=False),
    ]
    delete_reqs = conn.connection.send_compound(
        [x[0] for x in delete_msgs],
        sid=conn.session.session_id,
        tid=tree.tree_connect_id,
        related=True,
    )
    for i, request in enumerate(delete_reqs):
        # A SMBResponseException will be raised if something went wrong
        response = delete_msgs[i][1](request)


def delete_directory(
    path, share="C$", conn=None, host=None, username=None, password=None
):
    if HAS_SMBPROTOCOL:
        return _delete_directory_smbprotocol(
            path, share, conn=conn, host=host, username=username, password=password
        )
    raise MissingSmb("SMB library required (impacket or smbprotocol)")
