import json
import os
import time
import traceback
import zipfile
from contextlib import nullcontext
from datetime import datetime, timedelta, timezone

from .. import util
from ..util import Info
from .host import Host


class Exporter():
    """Main class for generating exports.
    """
    def __init__(self, output, book, *, singleton=False):
        self.output = output
        self.book = book
        self.singleton = singleton

        self.recycle = None
        self.used_ts = None
        self.map_id_to_eid = None

    def run(self, item_ids=None, recursive=False):
        book = self.book
        self.book.load_meta_files()
        self.book.load_toc_files()

        os.makedirs(self.output, exist_ok=True)

        self.recycle = set(book.toc.get('recycle', []))
        self.used_ts = set()
        self.map_id_to_eid = {}

        if item_ids:
            id_pool = {id for id in item_ids if id in book.meta and id not in self.recycle}

            # add descendant id if recursive mode
            if recursive:
                for id in list(id_pool):
                    for desc_id in self._iter_child_items(id, [id]):
                        id_pool.add(desc_id)
                        yield Info('debug', f'Included descendant item of "{id}": "{desc_id}"')
        else:
            id_pool = {id for id in book.meta if id not in self.recycle}

        id_chain = ['root']
        for id in self._iter_child_items('root', id_chain):
            if id in id_pool:
                yield from self._export_item(id, id_chain)

        id_chain = ['hidden']
        for id in self._iter_child_items('hidden', id_chain):
            if id in id_pool:
                yield from self._export_item(id, id_chain)

    def _iter_child_items(self, id, id_chain):
        """Generate descendant items for the item of id.
        """
        for ref_id in self.book.toc.get(id, []):
            yield ref_id

            # do not export children of a circular item
            if ref_id not in id_chain:
                id_chain.append(ref_id)
                yield from self._iter_child_items(ref_id, id_chain)
                id_chain.pop()

    def _export_item(self, id, id_chain):
        if id in self.map_id_to_eid:
            if self.singleton:
                yield Info('debug', f'Skipped exporting item "{id}" (singleton mode)')
                return

        yield Info('debug', f'Exporting item "{id}"')
        try:
            yield from self._export_item_internal(id, id_chain)
        except Exception as exc:
            # unexpected error
            traceback.print_exc()
            yield Info('error', f'Failed to export "{id}": {exc}', exc=exc)

    def _export_item_internal(self, id, id_chain):
        meta = self.book.meta[id]
        index = meta.get('index', '')

        # generate a unique timestamp as prefix
        ts = datetime.now(timezone.utc)
        ets = util.datetime_to_id(ts)
        while ets in self.used_ts:
            ts += timedelta(milliseconds=1)
            ets = util.datetime_to_id(ts)
        self.used_ts.add(ets)

        # setup an export id (eid), which is unique and is same among all
        # occurrences of the same id, to the ets of the first occurrence
        eid = self.map_id_to_eid.setdefault(id, ets)

        # generate a unique timestamp prefix
        basename = ets + '-' + meta.get('title', meta.get('id', ''))
        basename = util.crop(util.validate_filename(basename), 128)

        # generate a unique filename
        i = 0
        dst = os.path.join(self.output, f'{basename}.wsba')
        while os.path.lexists(dst):
            i += 1
            dst = os.path.join(self.output, f'{basename}-{i}.wsba')

        yield Info('info', f'Exporting "{id}" to "{os.path.basename(dst)}"')
        parents = [{'id': id, 'title': self.book.meta.get(id, {}).get('title', '')} for id in id_chain]
        meta_data = {'id': id, **meta}
        export_data = {
            'version': 1,
            'id': eid,
            'timestamp': ets,
            'timezone': datetime.now().astimezone().utcoffset().total_seconds(),
            'path': parents,
        }
        with zipfile.ZipFile(dst, 'w') as zh:
            zh.writestr('meta.json', json.dumps(meta_data, ensure_ascii=False, indent=2))
            zh.writestr('export.json', json.dumps(export_data, ensure_ascii=False, indent=2))

            # include data file(s)
            if index:
                zh.writestr('data/', '')
                src = os.path.join(self.book.data_dir, os.path.dirname(index) if index.endswith('/index.html') else index)
                yield Info('debug', f'Saving data files for "{id}": "{self.book.get_subpath(src)}"')
                util.zip_compress(zh, src, f'data/{os.path.basename(src)}')

            # include favicon cache
            iconfile = self.book.get_icon_file(meta)
            if not iconfile:
                return

            favicon_dir = os.path.join(self.book.tree_dir, 'favicon', '')
            if not os.path.normcase(iconfile).startswith(os.path.normcase(favicon_dir)):
                return

            zh.writestr('favicon/', '')
            util.zip_compress(zh, iconfile, f'favicon/{os.path.basename(iconfile)}')


def run(root, output, book_id='', item_ids=None, *, recursive=False, singleton=False,
        config=None, no_lock=False):
    start = time.time()

    host = Host(root, config)

    # Fail for invalid book ID
    if book_id not in host.books:
        yield Info('error', f'Invalid book "{book_id}".')
        return

    yield Info('debug', f'Loading book "{book_id}".')

    try:
        book = host.books[book_id]

        if book.no_tree:
            yield Info('error', f'Unable to export book "{book_id}" ("{book.name}") (no_tree).')
            return

        yield Info('info', f'Exporting from book "{book_id}" ({book.name}).')
        lh = nullcontext() if no_lock else book.get_tree_lock().acquire()
        with lh:
            generator = Exporter(output, book, singleton=singleton)
            yield from generator.run(item_ids, recursive)

    except Exception as exc:
        traceback.print_exc()
        yield Info('critical', str(exc), exc=exc)
    else:
        yield Info('info', 'Done.')

    yield Info('info', '----------------------------------------------------------------------')

    elapsed = time.time() - start
    yield Info('info', f'Time spent: {elapsed} seconds.')
