"""
Comic archive.

Reads and writes metadata via the included metadata package.
Reads data using libarchive via archi.
"""

import zipfile

from logging import getLogger
from pathlib import Path

import rarfile

from comicbox.config import get_config
from comicbox.exceptions import UnsupportedArchiveTypeError
from comicbox.logging import init_logging
from comicbox.metadata import comicapi
from comicbox.metadata.comet import CoMet
from comicbox.metadata.comic_base import IMAGE_EXT_RE
from comicbox.metadata.comic_base import ComicBaseMetadata
from comicbox.metadata.comic_xml import ComicXml
from comicbox.metadata.comicbookinfo import ComicBookInfo
from comicbox.metadata.comicinfoxml import ComicInfoXml
from comicbox.metadata.filename import FilenameMetadata


RECOMPRESS_SUFFIX = ".comicbox_tmp_zip"
CBZ_SUFFIX = ".cbz"
init_logging()
LOG = getLogger(__name__)


class ComicArchive(object):
    """
    Represent a comic archive.

    Contains the compressed archive file and its parsed metadata
    """

    PARSER_CLASSES = (ComicInfoXml, ComicBookInfo, CoMet)
    FILENAMES = set((CoMet.FILENAME, ComicInfoXml.FILENAME))

    def __init__(self, path, metadata=None, config=None):
        """Initialize the archive with a path to the archive."""
        if config is None:
            config = get_config()
        self.config = config
        self.set_path(path)
        self.metadata = ComicBaseMetadata(metadata=metadata)
        self.raw = {}
        self.cover_image_data = None
        if metadata is None and config.metadata:
            self._parse_metadata()

    def set_path(self, path):
        """Set the path and determine the archive type."""
        self._path = Path(path)
        if zipfile.is_zipfile(self._path):
            self.archive_cls = zipfile.ZipFile
        elif rarfile.is_rarfile(self._path):
            self.archive_cls = rarfile.RarFile
        else:
            raise UnsupportedArchiveTypeError(f"Unsupported archive type: {self._path}")

    def _get_archive(self, mode="r"):
        return self.archive_cls(self._path, mode=mode)  # type: ignore

    def get_path(self):
        """Get the path for the archive."""
        return self._path

    def namelist(self):
        """Get the archive file namelist."""
        with self._get_archive() as archive:
            return sorted(archive.namelist())

    def _parse_metadata_entries(self, archive):
        """Get the filenames and file based metadata."""
        cix_md = {}
        comet_md = {}
        for fn in sorted(archive.namelist()):
            basename = Path(fn).name.lower()
            xml_parser_cls = None
            if (
                basename == str(ComicInfoXml.FILENAME).lower()
                and self.config.comicinfoxml
            ):
                md = cix_md
                xml_parser_cls = ComicInfoXml
                title = "ComicInfo.xml"
            elif basename == str(CoMet.FILENAME).lower() and self.config.comet:
                md = comet_md
                xml_parser_cls = CoMet
                title = "CoMet.xml"
            else:
                continue
            data = archive.read(fn)
            if self.config.raw:
                self.raw[title] = data
            parser = xml_parser_cls(string=data)
            md.update(parser.metadata)
        return cix_md, comet_md

    def _get_archive_comment(self, archive):
        """Get the comment field from an archive."""
        if not archive:
            archive = self._get_archive()
        comment = archive.comment
        if isinstance(comment, bytes):
            comment = comment.decode()
        return comment

    def _parse_metadata_comments(self, archive):
        if not self.config.comicbookinfo:
            return {}
        comment = self._get_archive_comment(archive)
        parser = ComicBookInfo(string=comment)
        cbi_md = parser.metadata
        if self.config.raw:
            self.raw["ComicBookInfo Archive Comment"] = comment
        return cbi_md

    def _parse_metadata_filename(self):
        if not self.config.filename:
            return {}
        parser = FilenameMetadata(path=self._path)
        if self.config.raw:
            self.raw["Filename"] = self._path.name
        return parser.metadata

    def _parse_metadata(self):
        with self._get_archive() as archive:
            cix_md, comet_md = self._parse_metadata_entries(archive)
            cbi_md = self._parse_metadata_comments(archive)

            # order of the md list is very important, lowest to highest
            # precedence.
            filename_md = self._parse_metadata_filename()
            md_list = (filename_md, comet_md, cbi_md, cix_md)
            self.metadata.synthesize_metadata(md_list)
            namelist = sorted(archive.namelist())
            self.metadata.compute_page_metadata(namelist)
            if self.config.cover:
                self.cover_image_data = self._get_cover_image(archive)

    def get_num_pages(self):
        """Return the number of pages."""
        return self.metadata.get_num_pages()

    def _ensure_page_metadata(self, archive):
        """Ensure page metadata exists."""
        if not not self.metadata.metadata.get("page_count"):
            self.metadata.compute_page_metadata(sorted(archive.namelist()))

    def get_pages(self, page_from):
        """Generate all pages starting with page number."""
        with self._get_archive() as archive:
            self._ensure_page_metadata(archive)
            pagenames = self.metadata.get_pagenames_from(page_from)
            for pagename in pagenames:
                with archive.open(pagename) as page:
                    yield page.read()

    def get_page_by_filename(self, filename):
        """Return data for a single page by filename."""
        with self._get_archive() as archive:
            with archive.open(filename) as page:
                return page.read()

    def get_page_by_index(self, index):
        """Get the page data by index."""
        with self._get_archive() as archive:
            self._ensure_page_metadata(archive)
            filename = self.metadata.get_pagename(index)
            with archive.open(filename) as page:
                return page.read()

    def extract_pages(self, page_from, root_path="."):
        """Extract pages from archive and write to a path."""
        with self._get_archive() as archive:
            self._ensure_page_metadata(archive)
            filenames = self.metadata.get_pagenames_from(page_from)
            for fn in filenames:
                with archive.open(fn) as page:
                    fn = root_path / fn
                    with open(fn, "wb") as page_file:
                        page_file.write(page.read())

    def extract_cover_as(self, path):
        """Extract the cover image to a destination file."""
        with self._get_archive() as archive:
            self._ensure_page_metadata(archive)
            cover_fn = self.metadata.get_cover_page_filename()
            if not cover_fn:
                return
            with archive.open(cover_fn) as page:
                with open(path, "wb") as cover_file:
                    cover_file.write(page.read())

    def _get_cover_image(self, archive):
        """Return cover image data."""
        self._ensure_page_metadata(archive)
        cover_fn = self.metadata.get_cover_page_filename()
        if not cover_fn:
            return
        try:
            data = archive.read(cover_fn)
        except Exception as exc:
            LOG.error(f"{self._path} reading cover: {cover_fn}")
            raise exc
        return data

    def get_cover_image(self):
        """Get the cover image."""
        if not self.cover_image_data:
            with self._get_archive() as archive:
                self.cover_image_data = self._get_cover_image(archive)
        return self.cover_image_data

    def get_metadata(self):
        """Return the metadata from the archive."""
        return self.metadata.metadata

    def recompress(self, filename=None, data=None):
        """Recompress the archive optionally replacing a file."""
        new_path = self._path.with_suffix(CBZ_SUFFIX)
        if new_path.is_file() and new_path != self._path:
            raise ValueError(f"{new_path} already exists.")

        tmp_path = self._path.with_suffix(RECOMPRESS_SUFFIX)
        with self._get_archive() as archive:
            if self.config.delete_tags:
                comment = b""
            else:
                comment = archive.comment
            if isinstance(comment, str):
                comment = comment.encode()
            with zipfile.ZipFile(
                tmp_path, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9
            ) as zf:
                skipnames = set()
                if filename:
                    skipnames.add(filename)
                if self.config.delete_tags:
                    skipnames.add(self.FILENAMES)
                for info in sorted(archive.infolist(), key=lambda i: i.filename):
                    if info.filename.lower() in skipnames:
                        continue
                    if IMAGE_EXT_RE.search(info.filename) is None:
                        compress = zipfile.ZIP_DEFLATED
                    else:
                        # images usually end up slightly larger with
                        # zip compression
                        compress = zipfile.ZIP_STORED
                    zf.writestr(
                        info.filename,
                        archive.read(info),
                        compress_type=compress,
                        compresslevel=9,
                    )
                if filename and data:
                    zf.writestr(filename, data)
                if comment:
                    zf.comment = comment

        old_path = self._path
        tmp_path.replace(new_path)
        self._path = new_path
        if self.config.delete_rar:
            LOG.info(f"converted to: {new_path}")
            if new_path.is_file():
                old_path.unlink()
                LOG.info(f"removed: {old_path}")

    def write_metadata(self, md_class, recompute_page_sizes=True):
        """Write metadata using the supplied parser class."""
        parser = md_class(metadata=self.get_metadata())
        if recompute_page_sizes and isinstance(parser, ComicInfoXml):
            self.compute_pages_tags()
        if isinstance(parser, (ComicXml, CoMet)):
            self.recompress(parser.FILENAME, parser.to_string())
        elif isinstance(parser, ComicBookInfo):
            with self._get_archive("a") as archive:
                comment = parser.to_string().encode()
                archive.comment = comment
        else:
            raise ValueError(f"Unsupported metadata writer {md_class}")

    def to_comicapi(self):
        """Export to comicapi style metadata."""
        return comicapi.export(self.get_metadata())

    def import_file(self, filename):
        """Try to import metada from a file and then write it into the comic."""
        from xml.etree.ElementTree import ParseError

        from simplejson.errors import JSONDecodeError

        path = Path(filename)
        success_class = None
        md = None
        for cls in self.PARSER_CLASSES:
            try:
                md = cls(path=path)
                success_class = cls
                break
            except (ParseError, JSONDecodeError):
                pass
        if success_class and md:
            self.metadata.metadata = md.metadata
            self.write_metadata(success_class)

    def export_files(self):
        """Export metadata to all supported file formats."""
        for parser_cls in self.PARSER_CLASSES:
            md = parser_cls(self.get_metadata())
            path = Path(str(parser_cls.FILENAME))
            md.to_file(path)

    def compute_pages_tags(self):
        """Recompute the tag image sizes for ComicRack."""
        with self._get_archive() as archive:
            infolist = archive.infolist()
        parser = ComicInfoXml(metadata=self.get_metadata())
        parser.compute_pages_tags(infolist)
        self.metadata.metadata["pages"] = parser.metadata.get("pages")

    def compute_page_count(self):
        """Compute the page count from images in the archive."""
        self.metadata.compute_page_count()

    def rename_file(self):
        """Rename the archive."""
        car = FilenameMetadata(self.metadata)
        self._path = car.to_file(self._path)

    def print_raw(self):
        """Print raw metadtata."""
        for key, val in self.raw.items():
            print("-" * 10, key, "-" * 10)
            if isinstance(val, bytes):
                val = val.decode()
            print(val)
