#!/usr/pkg/bin/python3.13
# License: GPLv3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>

import json
import os
import sys

from calibre import browser, prints
from calibre.constants import __appname__, __version__, iswindows
from calibre.db.cli import module_for_cmd
from calibre.db.legacy import LibraryDatabase
from calibre.utils.config import OptionParser, prefs
from calibre.utils.localization import localize_user_manual_link
from calibre.utils.lock import singleinstance
from calibre.utils.serialize import MSGPACK_MIME
from polyglot import http_client
from polyglot.urllib import urlencode, urlparse, urlunparse

COMMANDS = (
    'list', 'add', 'remove', 'add_format', 'remove_format', 'show_metadata',
    'set_metadata', 'export', 'catalog', 'saved_searches', 'add_custom_column',
    'custom_columns', 'remove_custom_column', 'set_custom', 'restore_database',
    'check_library', 'list_categories', 'backup_metadata', 'clone', 'embed_metadata',
    'search'
)


def option_parser_for(cmd, args=()):

    def cmd_option_parser():
        return module_for_cmd(cmd).option_parser(get_parser, args)

    return cmd_option_parser


def run_cmd(cmd, opts, args, dbctx):
    m = module_for_cmd(cmd)
    if dbctx.is_remote and getattr(m, 'no_remote', False):
        raise SystemExit(_('The {} command is not supported with remote (server based) libraries').format(cmd))
    ret = m.main(opts, args, dbctx)
    return ret


def get_parser(usage):
    parser = OptionParser(usage)
    go = parser.add_option_group(_('GLOBAL OPTIONS'))
    go.is_global_options = True
    go.add_option(
        '--library-path',
        '--with-library',
        default=None,
        help=_(
            'Path to the calibre library. Default is to use the path stored in the settings.'
            ' You can also connect to a calibre Content server to perform actions on'
            ' remote libraries. To do so use a URL of the form: http://hostname:port/#library_id'
            ' for example, http://localhost:8080/#mylibrary. library_id is the library id'
            ' of the library you want to connect to on the Content server. You can use'
            ' the special library_id value of - to get a list of library ids available'
            ' on the server. For details on how to setup access via a Content server, see'
            ' {}.'
        ).format(localize_user_manual_link(
            'https://manual.calibre-ebook.com/generated/en/calibredb.html'
        ))
    )
    go.add_option(
        '-h', '--help', help=_('show this help message and exit'), action='help'
    )
    go.add_option(
        '--version',
        help=_("show program's version number and exit"),
        action='version'
    )
    go.add_option(
        '--username',
        help=_('Username for connecting to a calibre Content server')
    )
    go.add_option(
        '--password',
        help=_('Password for connecting to a calibre Content server.'
               ' To read the password from standard input, use the special value: {0}.'
               ' To read the password from a file, use: {1} (i.e. <f: followed by the full path to the file and a trailing >).'
               ' The angle brackets in the above are required, remember to escape them or use quotes'
               ' for your shell.').format(
                   '<stdin>', '<f:C:/path/to/file>' if iswindows else '<f:/path/to/file>')
    )
    go.add_option(
        '--timeout',
        type=float,
        default=120,
        help=_('The timeout, in seconds, when connecting to a calibre library over the network. The default is'
               ' two minutes.')
    )

    return parser


def option_parser():
    return get_parser(
        _(
            '''\
%%prog command [options] [arguments]

%%prog is the command line interface to the calibre books database.

command is one of:
  %s

For help on an individual command: %%prog command --help
'''
        ) % '\n  '.join(COMMANDS)
    )


def read_credentials(opts):
    username = opts.username
    pw = opts.password
    if pw:
        if pw == '<stdin>':
            from getpass import getpass
            pw = getpass(_('Enter the password: '))
        elif pw.startswith('<f:') and pw.endswith('>'):
            with lopen(pw[3:-1], 'rb') as f:
                pw = f.read().decode('utf-8').rstrip()
    return username, pw


class DBCtx:

    def __init__(self, opts):
        self.library_path = opts.library_path or prefs['library_path']
        self.timeout = opts.timeout
        self.url = None
        if self.library_path is None:
            raise SystemExit(
                'No saved library path, either run the GUI or use the'
                ' --with-library option'
            )
        if self.library_path.partition(':')[0] in ('http', 'https'):
            parts = urlparse(self.library_path)
            self.library_id = parts.fragment or None
            self.url = urlunparse(parts._replace(fragment='')).rstrip('/')
            self.br = browser(handle_refresh=False, user_agent=f'{__appname__} {__version__}')
            self.is_remote = True
            username, password = read_credentials(opts)
            self.has_credentials = False
            if username and password:
                self.br.add_password(self.url, username, password)
                self.has_credentials = True
            if self.library_id == '-':
                self.list_libraries()
                raise SystemExit()
        else:
            self.library_path = os.path.expanduser(self.library_path)
            if not singleinstance('db'):
                ext = '.exe' if iswindows else ''
                raise SystemExit(_(
                    'Another calibre program such as {} or the main calibre program is running.'
                    ' Having multiple programs that can make changes to a calibre library'
                    ' running at the same time is a bad idea. calibredb can connect directly'
                    ' to a running calibre Content server, to make changes through it, instead.'
                    ' See the documentation of the {} option for details.'
                ).format('calibre-server' + ext, '--with-library')
                )
            self._db = None
            self.is_remote = False

    @property
    def db(self):
        if self._db is None:
            self._db = LibraryDatabase(self.library_path)
        return self._db

    def path(self, path):
        if self.is_remote:
            with lopen(path, 'rb') as f:
                return path, f.read()
        return path

    def run(self, name, *args):
        m = module_for_cmd(name)
        if self.is_remote:
            return self.remote_run(name, m, *args)
        return m.implementation(self.db.new_api, None, *args)

    def interpret_http_error(self, err):
        if err.code == http_client.UNAUTHORIZED:
            if self.has_credentials:
                raise SystemExit('The username/password combination is incorrect')
            raise SystemExit('A username and password is required to access this server')
        if err.code == http_client.FORBIDDEN:
            raise SystemExit(err.reason)
        if err.code == http_client.NOT_FOUND:
            raise SystemExit(err.reason)

    def remote_run(self, name, m, *args):
        from mechanize import HTTPError, Request
        from calibre.utils.serialize import msgpack_loads, msgpack_dumps
        url = self.url + '/cdb/cmd/{}/{}'.format(name, getattr(m, 'version', 0))
        if self.library_id:
            url += '?' + urlencode({'library_id':self.library_id})
        rq = Request(url, data=msgpack_dumps(args),
                     headers={'Accept': MSGPACK_MIME, 'Content-Type': MSGPACK_MIME})
        try:
            res = self.br.open_novisit(rq, timeout=self.timeout)
            ans = msgpack_loads(res.read())
        except HTTPError as err:
            self.interpret_http_error(err)
            raise
        if 'err' in ans:
            if ans['tb']:
                prints(ans['tb'])
            raise SystemExit(ans['err'])
        return ans['result']

    def list_libraries(self):
        from mechanize import HTTPError
        url = self.url + '/ajax/library-info'
        try:
            res = self.br.open_novisit(url, timeout=self.timeout)
            ans = json.loads(res.read())
        except HTTPError as err:
            self.interpret_http_error(err)
            raise
        library_map, default_library = ans['library_map'], ans['default_library']
        for lid in sorted(library_map, key=lambda lid: (lid != default_library, lid)):
            prints(lid)


def main(args=sys.argv):
    parser = option_parser()
    if len(args) < 2:
        parser.print_help()
        return 1
    if args[1] in ('-h', '--help'):
        parser.print_help()
        return 0
    if args[1] == '--version':
        parser.print_version()
        return 0
    for i, x in enumerate(args):
        if i > 0 and x in COMMANDS:
            cmd = x
            break
    else:
        parser.print_help()
        return 1
    del args[i]
    parser = option_parser_for(cmd, args[1:])()
    opts, args = parser.parse_args(args)
    return run_cmd(cmd, opts, args[1:], DBCtx(opts))


if __name__ == '__main__':
    main()
