# -*- coding: utf-8 -*-

# Copyright © 2012-2024 Roberto Alsina and others.

# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
# documentation files (the "Software"), to deal in the
# Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the
# Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice
# shall be included in all copies or substantial portions of
# the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""Create a new post."""

import io
import datetime
import operator
import os
import shutil
import subprocess
import sys

import dateutil.tz
from blinker import signal

from nikola.plugin_categories import Command
from nikola import utils

COMPILERS_DOC_LINK = 'https://getnikola.com/handbook.html#configuring-other-input-formats'
POSTLOGGER = utils.get_logger('new_post')
PAGELOGGER = utils.get_logger('new_page')
LOGGER = POSTLOGGER


def get_default_compiler(is_post, compilers, post_pages):
    """Given compilers and post_pages, return a reasonable default compiler for this kind of post/page."""
    # First throw away all the post_pages with the wrong is_post
    filtered = [entry for entry in post_pages if entry[3] == is_post]

    # Get extensions in filtered post_pages until one matches a compiler
    for entry in filtered:
        extension = os.path.splitext(entry[0])[-1]
        for compiler, extensions in compilers.items():
            if extension in extensions:
                return compiler
    # No idea, back to default behaviour
    return 'rest'


def get_date(schedule=False, rule=None, last_date=None, tz=None, iso8601=False):
    """Return a date stamp, given a recurrence rule.

    schedule - bool:
        whether to use the recurrence rule or not

    rule - str:
        an iCal RRULE string that specifies the rule for scheduling posts

    last_date - datetime:
        timestamp of the last post

    tz - tzinfo:
        the timezone used for getting the current time.

    iso8601 - bool:
        whether to force ISO 8601 dates (instead of locale-specific ones)

    """
    if tz is None:
        tz = dateutil.tz.tzlocal()
    date = now = datetime.datetime.now(tz)
    if schedule:
        try:
            from dateutil import rrule
        except ImportError:
            LOGGER.error('To use the --schedule switch of new_post, '
                         'you have to install the "dateutil" package.')
            rrule = None
    if schedule and rrule and rule:
        try:
            rule_ = rrule.rrulestr(rule, dtstart=last_date or date)
        except Exception:
            LOGGER.error('Unable to parse rule string, using current time.')
        else:
            date = rule_.after(max(now, last_date or now), last_date is None)

    offset = tz.utcoffset(now)
    offset_sec = (offset.days * 24 * 3600 + offset.seconds)
    offset_hrs = offset_sec // 3600
    offset_min = offset_sec % 3600
    if iso8601:
        tz_str = '{0:+03d}:{1:02d}'.format(offset_hrs, offset_min // 60)
    else:
        if offset:
            tz_str = ' UTC{0:+03d}:{1:02d}'.format(offset_hrs, offset_min // 60)
        else:
            tz_str = ' UTC'

    return (date.strftime('%Y-%m-%d %H:%M:%S') + tz_str, date)


class CommandNewPost(Command):
    """Create a new post."""

    name = "new_post"
    doc_usage = "[options] [path]"
    doc_purpose = "create a new blog post or site page"
    cmd_options = [
        {
            'name': 'is_page',
            'short': 'p',
            'long': 'page',
            'type': bool,
            'default': False,
            'help': 'Create a page instead of a blog post. (see also: `nikola new_page`)'
        },
        {
            'name': 'title',
            'short': 't',
            'long': 'title',
            'type': str,
            'default': '',
            'help': 'Title for the post.'
        },
        {
            'name': 'author',
            'short': 'a',
            'long': 'author',
            'type': str,
            'default': '',
            'help': 'Author of the post.'
        },
        {
            'name': 'tags',
            'long': 'tags',
            'type': str,
            'default': '',
            'help': 'Comma-separated tags for the post.'
        },
        {
            'name': 'onefile',
            'short': '1',
            'type': bool,
            'default': False,
            'help': 'Create the post with embedded metadata (single file format)'
        },
        {
            'name': 'twofile',
            'short': '2',
            'type': bool,
            'default': False,
            'help': 'Create the post with separate metadata (two file format)'
        },
        {
            'name': 'edit',
            'short': 'e',
            'type': bool,
            'default': False,
            'help': 'Open the post (and meta file, if any) in $EDITOR after creation.'
        },
        {
            'name': 'content_format',
            'short': 'f',
            'long': 'format',
            'type': str,
            'default': '',
            'help': 'Markup format for the post (use --available-formats for list)',
        },
        {
            'name': 'available-formats',
            'short': 'F',
            'long': 'available-formats',
            'type': bool,
            'default': False,
            'help': 'List all available input formats'
        },
        {
            'name': 'schedule',
            'short': 's',
            'type': bool,
            'default': False,
            'help': 'Schedule the post based on recurrence rule'
        },
        {
            'name': 'import',
            'short': 'i',
            'long': 'import',
            'type': str,
            'default': '',
            'help': 'Import an existing file instead of creating a placeholder'
        },
        {
            'name': 'date-path',
            'short': 'd',
            'long': 'date-path',
            'type': bool,
            'default': False,
            'help': 'Create post with date path (eg. year/month/day, see NEW_POST_DATE_PATH_FORMAT in config)'
        },
    ]

    def _execute(self, options, args):
        """Create a new post or page."""
        global LOGGER
        compiler_names = [p.name for p in self.site.plugin_manager.get_plugins_of_category("PageCompiler")]

        if len(args) > 1:
            print(self.help())
            return False
        elif args:
            path = args[0]
        else:
            path = None

        # Even though stuff was split into `new_page`, it’s easier to do it
        # here not to duplicate the code.
        is_page = options.get('is_page', False)
        is_post = not is_page
        content_type = 'page' if is_page else 'post'
        title = options['title'] or None
        author = options['author'] or ''
        tags = options['tags']
        onefile = options['onefile']
        twofile = options['twofile']
        import_file = options['import']
        wants_available = options['available-formats']
        date_path_opt = options['date-path']
        date_path_auto = self.site.config['NEW_POST_DATE_PATH'] and content_type == 'post'
        date_path_format = self.site.config['NEW_POST_DATE_PATH_FORMAT'].strip('/')
        post_type = options.get('type', 'text')

        if wants_available:
            self.print_compilers()
            return

        if is_page:
            LOGGER = PAGELOGGER
        else:
            LOGGER = POSTLOGGER

        if twofile:
            onefile = False
        if not onefile and not twofile:
            onefile = self.site.config.get('ONE_FILE_POSTS', True)

        content_format = options['content_format']
        content_subformat = None

        if "@" in content_format:
            content_format, content_subformat = content_format.split("@")

        if not content_format and path and not os.path.isdir(path):
            # content_format not specified. If path was given, use
            # it to guess (Issue #2798)
            extension = os.path.splitext(path)[-1]
            for compiler, extensions in self.site.config['COMPILERS'].items():
                if extension in extensions:
                    content_format = compiler
            if not content_format:
                LOGGER.error("Unknown {0} extension {1}, maybe you need to install a plugin or enable an existing one?".format(content_type, extension))
                return

        elif not content_format and import_file:
            # content_format not specified. If import_file was given, use
            # it to guess (Issue #2798)
            extension = os.path.splitext(import_file)[-1]
            for compiler, extensions in self.site.config['COMPILERS'].items():
                if extension in extensions:
                    content_format = compiler
            if not content_format:
                LOGGER.error("Unknown {0} extension {1}, maybe you need to install a plugin or enable an existing one?".format(content_type, extension))
                return

        elif not content_format:  # Issue #400
            content_format = get_default_compiler(
                is_post,
                self.site.config['COMPILERS'],
                self.site.config['post_pages'])

        elif content_format not in compiler_names:
            LOGGER.error("Unknown {0} format {1}, maybe you need to install a plugin or enable an existing one?".format(content_type, content_format))
            self.print_compilers()
            return

        compiler_plugin = self.site.plugin_manager.get_plugin_by_name(
            content_format, "PageCompiler").plugin_object

        # Guess where we should put this
        entry = self.filter_post_pages(content_format, is_post)

        if entry is False:
            return 1

        if import_file:
            print("Importing Existing {xx}".format(xx=content_type.title()))
            print("-----------------------\n")
        else:
            print("Creating New {xx}".format(xx=content_type.title()))
            print("-----------------\n")
        if title is not None:
            print("Title:", title)
        else:
            while not title:
                title = utils.ask('Title')

        if isinstance(title, bytes):
            try:
                title = title.decode(sys.stdin.encoding)
            except (AttributeError, TypeError):  # for tests
                title = title.decode('utf-8')

        title = title.strip()
        if not path:
            slug = utils.slugify(title, lang=self.site.default_lang)
        else:
            if isinstance(path, bytes):
                try:
                    path = path.decode(sys.stdin.encoding)
                except (AttributeError, TypeError):  # for tests
                    path = path.decode('utf-8')
            if os.path.isdir(path):
                # If the user provides a directory, add the file name generated from title (Issue #2651)
                slug = utils.slugify(title, lang=self.site.default_lang)
                pattern = os.path.basename(entry[0])
                suffix = pattern[1:]
                path = os.path.join(path, slug + suffix)
            else:
                slug = utils.slugify(os.path.splitext(os.path.basename(path))[0], lang=self.site.default_lang)

        if isinstance(author, bytes):
            try:
                author = author.decode(sys.stdin.encoding)
            except (AttributeError, TypeError):  # for tests
                author = author.decode('utf-8')

        # Calculate the date to use for the content
        # SCHEDULE_ALL is post-only (Issue #2921)
        schedule = options['schedule'] or (self.site.config['SCHEDULE_ALL'] and is_post)
        rule = self.site.config['SCHEDULE_RULE']
        self.site.scan_posts()
        timeline = self.site.timeline
        last_date = None if not timeline else timeline[0].date
        date, dateobj = get_date(schedule, rule, last_date, self.site.tzinfo, self.site.config['FORCE_ISO8601'])
        data = {
            'title': title,
            'slug': slug,
            'date': date,
            'tags': tags,
            'link': '',
            'description': '',
            'type': post_type,
        }

        if not path:
            pattern = os.path.basename(entry[0])
            suffix = pattern[1:]
            output_path = os.path.dirname(entry[0])
            if date_path_auto or date_path_opt:
                output_path += os.sep + dateobj.strftime(date_path_format)

            txt_path = os.path.join(output_path, slug + suffix)
            meta_path = os.path.join(output_path, slug + ".meta")
        else:
            if date_path_opt:
                LOGGER.warning("A path has been specified, ignoring -d")
            txt_path = os.path.join(self.site.original_cwd, path)
            meta_path = os.path.splitext(txt_path)[0] + ".meta"

        if (not onefile and os.path.isfile(meta_path)) or \
                os.path.isfile(txt_path):

            # Emit an event when a post exists
            event = dict(path=txt_path)
            if not onefile:  # write metadata file
                event['meta_path'] = meta_path
            signal('existing_' + content_type).send(self, **event)

            LOGGER.error("The title already exists!")
            LOGGER.info("Existing {0}'s text is at: {1}".format(content_type, txt_path))
            if not onefile:
                LOGGER.info("Existing {0}'s metadata is at: {1}".format(content_type, meta_path))
            return 8

        d_name = os.path.dirname(txt_path)
        utils.makedirs(d_name)
        metadata = {}
        if author:
            metadata['author'] = author
        metadata.update(self.site.config['ADDITIONAL_METADATA'])
        data.update(metadata)

        # ipynb plugin needs the Jupyter kernel info. We get the kernel name
        # from the content_subformat and pass it to the compiler in the metadata
        if content_format == "ipynb" and content_subformat is not None:
            metadata["jupyter_kernel"] = content_subformat

        # Override onefile if not really supported.
        if not compiler_plugin.supports_onefile and onefile:
            onefile = False
            LOGGER.warning('This compiler does not support one-file posts.')

        if onefile and import_file:
            with io.open(import_file, 'r', encoding='utf-8-sig') as fh:
                content = fh.read()
        elif not import_file:
            if is_page:
                content = self.site.MESSAGES[self.site.default_lang]["Write your page here."]
            else:
                content = self.site.MESSAGES[self.site.default_lang]["Write your post here."]

        if (not onefile) and import_file:
            # Two-file posts are copied  on import (Issue #2380)
            shutil.copy(import_file, txt_path)
        else:
            compiler_plugin.create_post(
                txt_path, content=content, onefile=onefile, title=title,
                slug=slug, date=date, tags=tags, is_page=is_page, type=post_type, **metadata)

        event = dict(path=txt_path)

        if not onefile:  # write metadata file
            with io.open(meta_path, "w+", encoding="utf8") as fd:
                fd.write(utils.write_metadata(data, comment_wrap=False, site=self.site))
            LOGGER.info("Your {0}'s metadata is at: {1}".format(content_type, meta_path))
            event['meta_path'] = meta_path
        LOGGER.info("Your {0}'s text is at: {1}".format(content_type, txt_path))

        signal('new_' + content_type).send(self, **event)

        if options['edit']:
            editor = os.getenv('EDITOR', '').split()
            to_run = editor + [txt_path]
            if not onefile:
                to_run.append(meta_path)
            if editor:
                subprocess.call(to_run)
            else:
                LOGGER.error('The $EDITOR environment variable is not set, cannot edit the post with \'-e\'.  Please edit the post manually.')

    def filter_post_pages(self, compiler, is_post):
        """Return the correct entry from post_pages.

        Information based on:
        * selected compilers
        * available compilers
        * post/page status
        """
        compilers = self.site.config['COMPILERS']
        post_pages = self.site.config['post_pages']
        compiler_objs = self.site.compilers

        # First throw away all the post_pages with the wrong is_post
        filtered = [entry for entry in post_pages if entry[3] == is_post]

        # These are the extensions supported by the required format
        extensions = compilers.get(compiler)
        if extensions is None:
            if compiler in compiler_objs:
                LOGGER.error("There is a {0} compiler available, but it's not set in your COMPILERS option.".format(compiler))
                LOGGER.info("Read more: {0}".format(COMPILERS_DOC_LINK))
            else:
                LOGGER.error('Unknown format {0}'.format(compiler))
                self.print_compilers()
            return False

        # Throw away the post_pages with the wrong extensions
        filtered = [entry for entry in filtered if any([ext in entry[0] for ext in
                                                        extensions])]

        if not filtered:
            type_name = "post" if is_post else "page"
            LOGGER.error("Can't find a way, using your configuration, to create "
                         "a {0} in format {1}. You may want to tweak "
                         "COMPILERS or {2}S in conf.py".format(
                             type_name, compiler, type_name.upper()))
            LOGGER.info("Read more: {0}".format(COMPILERS_DOC_LINK))

            return False
        return filtered[0]

    def print_compilers(self):
        """List all available compilers in a human-friendly format."""
        # We use compilers_raw, because the normal dict can contain
        # garbage coming from the translation candidate implementation.
        # Entries are in format: (name, extensions, used_in_post_pages)

        compilers_raw = self.site.config['_COMPILERS_RAW']

        used_compilers = []
        unused_compilers = []
        disabled_compilers = []

        for name, plugin in self.site.compilers.items():
            if name in compilers_raw:
                used_compilers.append([
                    name,
                    plugin.friendly_name or name,
                    compilers_raw[name],
                    True
                ])
            else:
                disabled_compilers.append([
                    name,
                    plugin.friendly_name or name,
                    (),
                    False
                ])

        for plugin in self.site.disabled_compilers.values():
            if plugin.name in compilers_raw:
                unused_compilers.append([
                    plugin.name,
                    plugin.friendly_name,
                    compilers_raw[plugin.name],
                    False
                ])
            else:
                disabled_compilers.append([
                    plugin.name,
                    plugin.friendly_name,
                    (),
                    False
                ])

        used_compilers.sort(key=operator.itemgetter(0))
        unused_compilers.sort(key=operator.itemgetter(0))
        disabled_compilers.sort(key=operator.itemgetter(0))

        # We also group the compilers by status for readability.
        parsed_list = used_compilers + unused_compilers + disabled_compilers

        print("Available input formats:\n")

        name_width = max([len(i[0]) for i in parsed_list] + [4])  # 4 == len('NAME')
        fname_width = max([len(i[1]) for i in parsed_list] + [11])  # 11 == len('DESCRIPTION')

        print((' {0:<' + str(name_width) + '}  {1:<' + str(fname_width) + '}  EXTENSIONS\n').format('NAME', 'DESCRIPTION'))

        for name, fname, extensions, used in parsed_list:
            flag = ' ' if used else '!'
            flag = flag if extensions else '~'

            extensions = ', '.join(extensions) if extensions else '(disabled: not in COMPILERS)'

            print(('{flag}{name:<' + str(name_width) + '}  {fname:<' + str(fname_width) + '}  {extensions}').format(flag=flag, name=name, fname=fname, extensions=extensions))

        print("""
    More compilers are available in the Plugins Index.

    Compilers marked with ! and ~ require additional configuration:
        ! not in the POSTS/PAGES tuples and any post scanners (unused)
        ~ not in the COMPILERS dict (disabled)
    Read more: {0}""".format(COMPILERS_DOC_LINK))
