import io
import logging
import stat
import sys
from pathlib import Path
from typing import IO, Optional, Union

from ratarmountcore.formats import FileFormatID, replace_format_check
from ratarmountcore.mountsource import FileInfo, MountSource
from ratarmountcore.mountsource.SQLiteIndexMountSource import SQLiteIndexMountSource
from ratarmountcore.SQLiteIndex import SQLiteIndex
from ratarmountcore.utils import overrides

try:
    import py7zr

    if not py7zr.__version__.startswith("1.0"):
        raise ImportError("Wrong version found!")

    from py7zr.io import Py7zIO, WriterFactory

    class CachedFile(Py7zIO):
        def __init__(self, offset: int = 0, maxSize: int = sys.maxsize):
            self.file = io.BytesIO()
            self._offset = offset
            self._maxSize = maxSize
            self._position = 0

        @overrides(Py7zIO)
        def write(self, s: Union[bytes, bytearray]) -> int:
            toSkip = self._offset - self._position
            self._position += len(s)
            if toSkip < len(s) and self.file.tell() < self._maxSize:
                toRead = self._maxSize - self.file.tell()
                self.file.write(s[toSkip : toSkip + toRead])
            return self._position

        @overrides(Py7zIO)
        def read(self, size: Optional[int] = None) -> bytes:
            raise NotImplementedError

        @overrides(Py7zIO)
        def flush(self) -> None:
            pass

        @overrides(Py7zIO)
        def seek(self, offset: int, whence: int = 0) -> int:  # pylint: disable=unused-argument
            return offset

        @overrides(Py7zIO)
        def size(self) -> int:
            return self.file.tell()

    class CachedFileFactory(WriterFactory):
        # This factory only seems so useless because we are only extracting single files.
        # It makes sense when extracting multiple files.
        def __init__(self, offset: int = 0, maxSize: int = sys.maxsize):
            self._offset = offset
            self._maxSize = maxSize
            self.products: dict[str, CachedFile] = {}

        def create(self, filename) -> Py7zIO:
            product = CachedFile(self._offset, self._maxSize)
            self.products[filename] = product
            return product

    def open_in_memory(archive, target: str, offset: int = 0, maxSize: int = sys.maxsize) -> IO[bytes]:
        factory = CachedFileFactory(offset=offset, maxSize=maxSize)
        archive.reset()
        archive.extract(targets=[target], factory=factory)
        file = factory.products[target].file
        file.seek(0)
        return file

    # https://github.com/miurahr/py7zr/issues/659#issuecomment-2954260661
    replace_format_check(FileFormatID.SEVEN_ZIP, py7zr.is_7zfile)  # type: ignore

except ImportError:
    py7zr = None  # type: ignore


logger = logging.getLogger(__name__)


class Py7zrMountSource(SQLiteIndexMountSource):
    def __init__(self, fileOrPath: Union[str, IO[bytes], Path], **options) -> None:
        if isinstance(fileOrPath, Path):
            fileOrPath = str(fileOrPath)

        # TODO I doubt that symbolic links work because py7zr.FileInfo does not have information regarding links.

        def open_file(password: Optional[Union[str, bytes]] = None):
            return py7zr.SevenZipFile(
                # https://github.com/miurahr/py7zr/issues/659#issuecomment-2954260661
                fileOrPath,  # type: ignore
                password=None if password is None else (password if isinstance(password, str) else password.decode()),
            )

        self.fileObject = Py7zrMountSource._find_password(open_file, options.get("passwords", []))

        # Force indexes in memory because:
        #  - I have no idea what ID to write into 'offset' or 'offsetheader'. The "ID" for the py7zr interface
        #    is the "filename" (path). Storing a string in the int 'offset' column is not a good idea.
        #  - The py7zr interface seems to lack a way to query information about symbolic links.
        indexOptions = {
            'indexFilePath': ':memory:',
            'archiveFilePath': fileOrPath if isinstance(fileOrPath, str) else None,
            'backendName': 'Py7zrMountSource',
        }
        super().__init__(**(options | indexOptions))
        self._finalize_index(
            lambda: self.index.set_file_infos([self._convert_to_row(info) for info in self.fileObject.list()])
        )

    def _convert_to_row(self, info) -> tuple:
        mode = 0o777 | (stat.S_IFDIR if info.is_directory else stat.S_IFREG)
        mtime = info.creationtime.timestamp()
        path, name = SQLiteIndex.normpath(self.transform(info.filename)).rsplit("/", 1)

        # fmt: off
        fileInfo : tuple = (
            path              ,  # 0  : path
            name              ,  # 1  : file name
            0                 ,  # 2  : header offset
            0                 ,  # 3  : data offset
            info.uncompressed ,  # 4  : file size
            mtime             ,  # 5  : modification time
            mode              ,  # 6  : file mode / permissions
            0                 ,  # 7  : TAR file type. Currently unused. Overlaps with mode
            # Abuse the linkname to store the "ID" for file access.
            info.filename     ,  # 8  : linkname
            0                 ,  # 9  : user ID
            0                 ,  # 10 : group ID
            False             ,  # 11 : is TAR (unused?)
            False             ,  # 12 : is sparse
            False             ,  # 13 : is generated (parent folder)
            0                 ,  # 14 : recursion depth
        )
        # fmt: on

        return fileInfo

    @staticmethod
    def _find_password(openFile, passwords):
        fileobj = openFile()

        # If headers are encrypted, then infolist will simply return an empty list!
        files = fileobj.list()
        if not files:
            for password in passwords:
                fileobj = openFile(password)
                files = fileobj.list()
                if files:
                    return fileobj

        # If headers are not encrypted, then try out passwords by trying to open the first file.
        files = [file for file in files if not file.is_directory and file.uncompressed > 0]
        if not files:
            return fileobj

        for password in [None, *passwords]:
            fileobj = openFile(password)
            try:
                open_in_memory(fileobj, files[0].filename)
                return fileobj
            except Exception:
                pass

        raise RuntimeError("Could not find a matching password!")

    @overrides(SQLiteIndexMountSource)
    def close(self) -> None:
        super().close()
        if fileObject := getattr(self, 'fileObject', None):
            fileObject.close()
            # Throws: AttributeError: 'SevenZipFile' object has no attribute 'worker'
            # on double close and has no 'closed' method -.-, so we have to keep track of it ourselves by
            # setting it to None after it was closed.
            self.fileObject = None

    @overrides(MountSource)
    def open(self, fileInfo: FileInfo, buffering=-1) -> IO[bytes]:
        return open_in_memory(self.fileObject, fileInfo.linkname)
