#!/usr/bin/env python
# coding=utf-8
from functools import wraps
import json
import os
from pprint import pformat
import re

import click
# dirty hack for locale bug (for docutils)
os.environ['LC_CTYPE'] = 'en_US.UTF8'
from docutils.core import publish_doctree, publish_from_doctree
from docutils.nodes import comment, Text, title as title_class
from rst2confluence import confluence
import requests
import requests.packages.urllib3
requests.packages.urllib3.disable_warnings()


autogen_warning = {
    'off': '',
    'en': (
        '{note:title=Autogenerated documentation}\n'
        'This page is autogenerated from RST source.\n'
        'All manual changes will be lost after next update.\n'
        '{note}\n'
    ),
    'ru': (
        '{note:title=Автогенерируемая документация}\n'
        'Эта страница сгенерирована из файла в формате RST.\n'
        'Изменения, внесённые в страницу, будут '
        'перезаписаны при следующем обновлении.\n'
        '{note}\n'
    )

}


def config_data(config):
    if not os.path.exists(config):
        return make_config(config)

    with open(config) as f:
        data = json.load(f)
    return data['url'], data['user'], data['password']


def make_config(default_path):
    path = click.prompt(
        'Configuration file location',
        type=click.Path(dir_okay=False, writable=True, resolve_path=True),
        default=default_path)
    url = click.prompt('Confluence URL', type=click.STRING)
    user = click.prompt('Confluence login', type=click.STRING)
    password = click.prompt(
        'Confluence password',
        type=click.STRING,
        hide_input=True)

    with open(path, 'w') as f:
        json.dump(
            {'url': url,
             'user': user,
             'password': password},
            f,
            indent=4)
    click.echo('Wrote configuration to {0}'.format(path))

    return url, user, password


def generate_content(filename, tip_lang):
    click.echo('Preparing content...')
    with open(filename) as f:
        rst = f.read()
    try:
        doctree = publish_doctree(rst)
        content = publish_from_doctree(doctree, writer=confluence.Writer())
    except Exception:
        click.echo('There was error on processing ReST file')
        raise click.Abort()

    metadata = extract_metadata(doctree)
    if metadata:
        tip_lang = tip_lang or metadata.get('warning')
    warning = autogen_warning.get(tip_lang, '')

    return warning + content, metadata


def extract_metadata(doctree):
    comments = find_comments(doctree)
    metadata = {}
    for c in comments:
        metadata = parse_metadata(c) or metadata

    if 'title' not in metadata:
        title = find_title(doctree)
        if title:
            metadata['inferred_title'] = title

    return metadata


def find_title(doctree):
    title = None
    candidate_nodes = list(doctree.traverse(condition=title_class))
    if candidate_nodes:
        text_child = list(
            candidate_nodes[0].traverse(condition=Text, include_self=False))
        if text_child:
            title = text_child[0].astext()

    return title


def find_comments(doctree):
    acc = []
    for comment_node in doctree.traverse(condition=comment):
        text_child = list(comment_node.traverse(condition=Text,
                                                include_self=False))
        if text_child:
            text = text_child[0].astext()
            acc.append(text)

    return acc


def parse_metadata(text):
    if text.startswith('rst2wiki'):
        pattern = '''
            # argument name enclosed in ':'
            :(?P<arg>page|ancestor|title|warning):
            # whitespace
            \s
            # argument value (all until end of line)
            (?P<val>.*)
        '''
        result = re.findall(pattern, text, flags=re.VERBOSE)
        return dict(result)
    else:
        return None


def prepare_for_sending(content, page, ancestor_page=None, title=None):
    meta = {
        'id': page['id'],
        'type': 'page',
        'title': title or page['title'],
        'space': {'key': page['space']['key']},
        'version': {'number': page['version']['number'] + 1},
        'body': {'wiki': {'value': content,
                          'representation': 'wiki'}}
    }

    if ancestor_page:
        meta['ancestors'] = [{'type': 'page', 'id': ancestor_page['id']}]

        if meta['space']['key'] != ancestor_page['space']['key']:
            raise click.ClickException(
                "Your ancestor page belongs to another space ({0} <> {1}).\n"
                "Because it is currently not possible to change "
                "page's space\nthrough Confluence REST API, "
                "you'll need to do it manually."
                .format(meta['space']['key'],
                        ancestor_page['space']['key']))

    return meta


def publish_content(content, page_id, ancestor_id=None,
                    title=None, api=None):
    page = api.fetch_page(page_id)
    ancestor_page = api.fetch_page(ancestor_id) if ancestor_id else None

    body = prepare_for_sending(content, page, ancestor_page, title)

    api.update_page(body)


def publish_content_on_new_page(content, ancestor_id=None,
                                title=None, api=None):
    ancestor_page = api.fetch_page(ancestor_id) if ancestor_id else None
    body = {
        'type': 'page',
        'title': title,
        'space': {
            'key': ancestor_page['space']['key'] if ancestor_page
                else api.default_space
        },
        'body': {
            'wiki': {
                'value': content,
                'representation': 'wiki',
            }
        },
    }
    if ancestor_id:
        body['ancestors'] = [{'type': 'page', 'id': ancestor_id}]

    api.create_page(body)


class ConfluenceAPI(object):
    """
    Wrapper for subset of Confluence REST API.
    """
    def __init__(self, hostname, user, password):
        self.hostname = hostname.rstrip('/')
        self.user = user
        self.password = password

        self.session = requests.Session()
        self.session.auth = (self.user, self.password)
        self.session.headers = {
            'Accept': 'application/json',
            'Content-Type': 'application/json'}

        self.content_endpoint = (
            self.hostname + '/rest/api/content'
        )

        self.default_space = '~' + self.user

    @property
    def auth(self):
        return (self.user, self.password)

    def error_handling(method):
        @wraps(method)
        def wrapped(*args, **kwargs):
            try:
                self = args[0]
                return method(*args, **kwargs)
            except requests.ConnectionError:
                raise click.ClickException(
                    'Could not connect to {0}'.format(self.hostname))
            except requests.RequestException as e:
                click.echo(
                    'Something went wrong, analyze response from server:')
                if (e.response.headers.get('Content-Type') ==
                        'application/json'):
                    click.echo(pformat(e.response.json()))
                else:
                    click.echo(pformat(e.response.text))
                raise click.Abort()

        return wrapped

    def page_url(self, page_id):
        return self.content_endpoint + '/{0}'.format(page_id)

    @error_handling
    def fetch_page(self, page_id):
        click.echo('Fetching page {0}...'.format(page_id))
        url = self.page_url(page_id)
        response = self.session.get(url)
        response.raise_for_status()
        return response.json()

    @error_handling
    def update_page(self, payload):
        page_id = payload['id']
        click.echo('Writing to Confluence...')
        response = self.session.put(
            self.page_url(page_id),
            data=json.dumps(payload))
        response.raise_for_status()
        click.echo('Page {0} successfully updated'.format(page_id))

    @error_handling
    def create_page(self, payload):
        click.echo('Writing to Confluence...')
        response = self.session.post(
            self.content_endpoint,
            data=json.dumps(payload))
        response.raise_for_status()
        click.echo('Page successfully created. Page id: {0}'
                   .format(response.json().get('id')))


@click.command()
@click.argument('source', type=click.Path(exists=True, dir_okay=False))
@click.option('--create', is_flag=True,
              help='Create new page, instead of updating existing page. '
                   'This option is ignored, if --page is provided')
@click.option('-p', '--page', type=click.INT,
              help='Page id in Confluence.')
@click.option('-a', '--ancestor', type=click.INT,
              help='Ancestor page id in Confluence.')
@click.option('-t', '--title', type=click.STRING,
              help='Page title in confluence.')
@click.option('-w', '--warning', type=click.Choice(autogen_warning.keys()),
              help='Language of autogenerated warning.')
@click.option('-c', '--config',
              type=click.Path(resolve_path=True),
              default=click.get_app_dir('rst2wiki', force_posix=True),
              help='Configuration file')
@click.version_option()
def main(source, create, page, ancestor, title, warning, config):
    """
    Tool converts SOURCE file in reStructuredText format to confluence
    wiki format and pushes it in Confluence instance.
    """
    content, metadata = generate_content(source, warning)
    if metadata:
        # arguments from command line have priority
        page = page or metadata.get('page')
        ancestor = ancestor or metadata.get('ancestor')
        title = title or metadata.get('title')
        # if new page and title is not specified, use first header
        if create and title is None:
            title = metadata.get('inferred_title')

    # we need page id for update
    if page is None and not create:
        raise click.BadParameter(
            'Please provide page id using option or document metadata',
            param_hint='-p/--page')
    # we need title for new page
    if create and title is None:
        raise click.BadParameter(
            'Please provide title using option or document metadata',
            param_hint='-t/--title')

    hostname, user, password = config_data(config)
    cfl = ConfluenceAPI(hostname, user, password)

    # if set option --page is set, then option --create is ignored
    if create and page is None:
        publish_content_on_new_page(content, ancestor, title, cfl)
    else:
        publish_content(content, page, ancestor, title, cfl)

if __name__ == '__main__':
    main()
