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


import os

from calibre import prints
from calibre.ebooks.metadata.book.base import field_from_string
from calibre.ebooks.metadata.book.serialize import read_cover
from calibre.ebooks.metadata.opf import get_metadata
from calibre.srv.changes import metadata
from polyglot.builtins import iteritems

readonly = False
version = 0  # change this if you change signature of implementation()


def implementation(db, notify_changes, action, *args):
    is_remote = notify_changes is not None
    if action == 'field_metadata':
        return db.field_metadata
    if action == 'opf':
        book_id, mi = args
        with db.write_lock:
            if not db.has_id(book_id):
                return
            changed_ids = db.set_metadata(book_id, mi, force_changes=True, allow_case_change=False)
            if is_remote:
                notify_changes(metadata(changed_ids))
            return db.get_metadata(book_id)
    if action == 'fields':
        book_id, fvals = args
        with db.write_lock:
            if not db.has_id(book_id):
                return
            mi = db.get_metadata(book_id)
            for field, val in fvals:
                if field.endswith('_index'):
                    sname = mi.get(field[:-6])
                    if sname:
                        mi.set(field[:-6], sname, extra=val)
                        if field == 'series_index':
                            mi.series_index = val  # extra has no effect for the builtin series field
                elif field == 'cover':
                    if is_remote:
                        mi.cover_data = None, val[1]
                    else:
                        mi.cover = val
                        read_cover(mi)
                else:
                    mi.set(field, val)
            changed_ids = db.set_metadata(book_id, mi, force_changes=True, allow_case_change=True)
            if is_remote:
                notify_changes(metadata(changed_ids))
            return db.get_metadata(book_id)


def option_parser(get_parser, args):
    parser = get_parser(
        _(
            '''
%prog set_metadata [options] book_id [/path/to/metadata.opf]

Set the metadata stored in the calibre database for the book identified by
book_id from the OPF file metadata.opf. book_id is a book id number from the
search command. You can get a quick feel for the OPF format by using the
--as-opf switch to the show_metadata command. You can also set the metadata of
individual fields with the --field option. If you use the --field option, there
is no need to specify an OPF file.
'''
        )
    )
    parser.add_option(
        '-f',
        '--field',
        action='append',
        default=[],
        help=_(
            'The field to set. Format is field_name:value, for example: '
            '{0} tags:tag1,tag2. Use {1} to get a list of all field names. You '
            'can specify this option multiple times to set multiple fields. '
            'Note: For languages you must use the ISO639 language codes (e.g. '
            'en for English, fr for French and so on). For identifiers, the '
            'syntax is {0} {2}. For boolean (yes/no) fields use true and false '
            'or yes and no.'
        ).format('--field', '--list-fields', 'identifiers:isbn:XXXX,doi:YYYYY')
    )
    parser.add_option(
        '-l',
        '--list-fields',
        action='store_true',
        default=False,
        help=_(
            'List the metadata field names that can be used'
            ' with the --field option'
        )
    )
    return parser


def get_fields(dbctx):
    fm = dbctx.run('set_metadata', 'field_metadata')
    for key in sorted(fm.all_field_keys()):
        m = fm[key]
        if (key not in {'formats', 'series_sort', 'ondevice', 'path',
            'last_modified'} and m['is_editable'] and m['name']):
            yield key, m
            if m['datatype'] == 'series':
                si = m.copy()
                si['name'] = m['name'] + ' Index'
                si['datatype'] = 'float'
                yield key + '_index', si
    c = fm['cover'].copy()
    c['datatype'] = 'text'
    yield 'cover', c


def main(opts, args, dbctx):
    if opts.list_fields:
        ans = get_fields(dbctx)
        prints('{:<40}'.format(_('Title')), _('Field name'), '\n')
        for key, m in ans:
            prints('{:<40}'.format(m['name']), key)
        return 0

    def verify_int(x):
        try:
            int(x)
            return True
        except:
            return False

    if len(args) < 1 or not verify_int(args[0]):
        raise SystemExit(_(
            'You must specify a record id as the '
            'first argument'
        ))
    if len(args) < 2 and not opts.field:
        raise SystemExit(_('You must specify either a field or an OPF file'))
    book_id = int(args[0])

    if len(args) > 1:
        opf = os.path.abspath(args[1])
        if not os.path.exists(opf):
            raise SystemExit(_('The OPF file %s does not exist') % opf)
        with open(opf, 'rb') as stream:
            mi = get_metadata(stream)[0]
        if mi.cover:
            mi.cover = os.path.join(os.path.dirname(opf), os.path.relpath(mi.cover, os.getcwd()))
        final_mi = dbctx.run('set_metadata', 'opf', book_id, read_cover(mi))
        if not final_mi:
            raise SystemExit(_('No book with id: %s in the database') % book_id)

    if opts.field:
        fields = dict(get_fields(dbctx))
        fields['title_sort'] = fields['sort']
        vals = {}
        for x in opts.field:
            field, val = x.partition(':')[::2]
            if field == 'sort':
                field = 'title_sort'
            if field not in fields:
                raise SystemExit(_('{} is not a known field').format(field))
            if field == 'cover':
                val = dbctx.path(os.path.abspath(os.path.expanduser(val)))
            else:
                val = field_from_string(field, val, fields[field])
            vals[field] = val
        fvals = []
        for field, val in sorted(  # ensure series_index fields are set last
                iteritems(vals), key=lambda k: 1 if k[0].endswith('_index') else 0):
            if field.endswith('_index'):
                try:
                    val = float(val)
                except Exception:
                    raise SystemExit(_('The value {!r} is not a valid series index').format(val))
            fvals.append((field, val))

        final_mi = dbctx.run('set_metadata', 'fields', book_id, fvals)
        if not final_mi:
            raise SystemExit(_('No book with id: %s in the database') % book_id)

    prints(str(final_mi))
    return 0
