from datetime import datetime
import logging
import os
import re
import shutil
import subprocess
import tempfile
import json
import requests

from six.moves.urllib.request import urlopen
import six
if six.PY3:
    unicode = str

import lxml.etree as ET
from flask import Flask, request, abort, make_response, render_template, redirect, url_for

from yandex.maps.wiki.log import setup_logging
from yandex.maps.wiki.utils import ConnParams

WIKI_PATH = '/usr/share/yandex/maps/wiki'
INIT_PATH = os.environ.get('INIT_PATH', os.path.join(WIKI_PATH, 'renderer/layers'))
LAYERS_PATH = os.environ.get('LAYERS_PATH', os.path.join(WIKI_PATH, 'designstand/layers'))

INIT_PATH_V2 = os.path.join(INIT_PATH, 'v2')
LAYERS_PATH_V2 = os.path.join(LAYERS_PATH, 'v2')

BRANCHES_PATH = os.path.join(LAYERS_PATH, 'branches')
RESTART_COMMANDS = ['sudo yacare restart wiki-renderer-designstand',
                    'sudo yacare restart wiki-editor-designstand']

RELOAD_DESIGN_URL = 'http://127.0.0.1/reload_design'
RENDERER_HOST = 'core-nmaps-designstand-renderer.maps.n.yandex.ru'

ROOT_PATH = os.environ.get('ROOT_PATH', '/usr/lib/yandex/maps/wiki/designstand/')
HTML_TEMPLATE_FOLDER = os.path.join(ROOT_PATH, 'html')

MAP_DESIGN_FILE = 'design.map.json'
SKL_DESIGN_FILE = 'design.skl.json'
MPMAP_DESIGN_FILE = 'design.mpmap.json'
MPSKL_DESIGN_FILE = 'design.mpskl.json'

g_layers = {}
g_style2_maps = {}

app = Flask(__name__, template_folder=HTML_TEMPLATE_FOLDER)
app.root_path = ROOT_PATH

RESOURCES_DIR_MODE = 0o755
MAPXML_FILE_MODE = 0o644

STYLE2_PRODUCTION = 'production'
STYLE2_TESTING = 'testing'


class Style2Map(object):
    def __init__(self, path):
        logging.info('Initialized style2 map from path = %s', path)
        self.path = path
        self.update()

    def update(self):
        revision_path = os.path.join(self.path, 'revision.json')
        self._revision = json.load(open(revision_path, 'r'))

    @property
    def revision_id(self):
        return self._revision['id']

    @property
    def name(self):
        return self._revision['name']

    @property
    def datetime(self):
        return self._revision['datetime']

    def revision(self):
        return self._revision


class Layer(object):
    def __init__(self, layer_id):
        logging.info('Initialized layer: %s', layer_id)
        self.layer_id = layer_id
        self.path = os.path.join(BRANCHES_PATH, layer_id)
        self.link_path = os.path.join(LAYERS_PATH, layer_id)

        if not os.path.exists(self.path):
            os.makedirs(self.path)

        self.branches = {}
        for branch_id in os.listdir(self.path):
            self.branches[branch_id] = Branch(os.path.join(self.path, branch_id))

    def create(self, branch_id):
        self.branches[branch_id] = Branch(os.path.join(self.path, branch_id))

    def reset(self, branch_id):
        self.branches[branch_id] = Branch(os.path.join(self.path, branch_id))
        self.branches[branch_id].reset(os.path.join(INIT_PATH, self.layer_id))

    def activate(self, branch_id):
        branch = self.branches[branch_id]
        if os.path.lexists(self.link_path):
            os.unlink(self.link_path)
        os.symlink(branch.path, self.link_path)

    def activated(self, branch_id):
        return \
            os.path.lexists(self.link_path) and \
            self.branches[branch_id].path == os.path.realpath(os.readlink(self.link_path))

    def remove(self, branch_id):
        if self.activated(branch_id):
            os.unlink(self.link_path)
        shutil.rmtree(self.branches[branch_id].path)
        del self.branches[branch_id]

    def copy(self, src_branch_id, dst_branch_id):
        dst_branch_path = os.path.join(self.path, dst_branch_id)
        src_branch_path = os.path.join(self.path, src_branch_id)
        if os.path.exists(dst_branch_path):
            shutil.rmtree(dst_branch_path)
        shutil.copytree(src_branch_path, dst_branch_path)

        self.branches.setdefault(dst_branch_id, Branch(dst_branch_path))


class Branch(object):
    def __init__(self, path):
        self.path = path
        if not os.path.exists(self.path):
            os.makedirs(self.path)

    @property
    def conn_string(self):
        config = ET.parse(open(os.path.join(self.path, 'config.xml')))
        config.xinclude()
        dbname = config.xpath('/config/db/@name')[0]
        conn_params = ConnParams.from_xml(
            config.xpath('/config/db/read')[0], dbname=dbname)
        # support another way of setting search_path in renderer config
        search_path = config.xpath('/config/searchPath/@default')
        if search_path:
            conn_params.options = '--search_path='+search_path[0]
        return conn_params.connstring()

    @property
    def map_xml(self):
        contents = open(os.path.join(self.path, 'map.xml')).read()
        return substitute_resource_prefix(contents, 'resources/', 'http://')

    @property
    def modified(self):
        max_timestamp = max(os.stat(os.path.join(root, fname)).st_mtime
                            for root, dirs, files in os.walk(self.path)
                            for fname in files)
        return datetime.fromtimestamp(max_timestamp)

    @map_xml.setter
    def map_xml(self, contents):
        if isinstance(contents, unicode):
            contents = contents.encode('utf8')

        tempdir = tempfile.mkdtemp()
        try:
            def callback(url):
                fname = os.path.join(tempdir, url[len('http://'):])
                fname = os.path.normpath(fname)
                if not fname.startswith(tempdir):
                    raise ValueError("bad resource name %s" % url)

                if os.path.exists(fname):
                    return

                logging.debug('Downloading resource url: %s', url)
                resource_contents = urlopen(url).read()
                if not os.path.exists(os.path.dirname(fname)):
                    os.makedirs(os.path.dirname(fname))
                with open(fname, 'wb') as f:
                    f.write(resource_contents)

            contents = substitute_resource_prefix(contents, 'http://', 'resources/',
                                                  callback=callback)

            destdir = os.path.join(self.path, 'resources')
            if os.path.exists(destdir):
                shutil.rmtree(destdir)
            shutil.move(tempdir, destdir)
            os.chmod(destdir, RESOURCES_DIR_MODE)
        except:
            shutil.rmtree(tempdir)
            raise

        atomic_write(os.path.join(self.path, 'map.xml'), contents)

    def reset(self, src_path):
        if os.path.exists(self.path):
            shutil.rmtree(self.path)
        shutil.copytree(src_path, self.path)


def substitute_resource_prefix(contents,
                               from_prefix, to_prefix,
                               callback=lambda url: None):
    def sub(match):
        old_url = match.group(1)
        if not old_url.startswith(from_prefix):
            raise ValueError("resource name %s does not start with %s"
                             % (old_url, from_prefix))
        else:
            new_url = to_prefix + old_url[len(from_prefix):]
            callback(old_url)
            return 'rs:filename="%s"' % new_url

    return re.sub(r'rs:filename="([^"]+)"', sub, contents)


def branch_exists(layer_id, branch_id):
    return layer_id in g_layers and branch_id in g_layers[layer_id].branches


def check_branch(layer_id, branch_id):
    if not branch_exists(layer_id, branch_id):
        abort(404, 'branch %s.%s not found' % (layer_id, branch_id))


def get_branch(layer_id, branch_id):
    check_branch(layer_id, branch_id)
    return g_layers[layer_id].branches[branch_id]


def create_branch(layer_id, branch_id):
    g_layers.setdefault(layer_id, Layer(layer_id)).create(branch_id)


def atomic_write(path, contents):
    with tempfile.NamedTemporaryFile(delete=False) as tmp:
        tmp.write(contents)
        tmp.flush()
        shutil.move(tmp.name, path)
        os.chmod(path, MAPXML_FILE_MODE)


def restart_services():
    for cmd in RESTART_COMMANDS:
        logging.info('Restarting service: %s', cmd)
        process = subprocess.Popen([cmd],
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)

        stdout, stderr = process.communicate()
        if process.wait() != 0:
            logging.error('Service restart fail:\n stdout: %s\n stderr: %s' % (stdout, stderr))

    logging.info('Services restarted')


@app.route('/layers/<layer_id>/<branch_id>/map')
def get_map(layer_id, branch_id):
    """http://wiki.yandex-team.ru/jandekskarty/development/fordevelopers/renderer/gui/KartypoHTTP"""
    branch = get_branch(layer_id, branch_id)
    feature = request.args.get('feature', 'xml')
    app.logger.info('Map requested, layer: %s branch: %s feature: %s',
                    layer_id, branch_id, feature)

    if feature == 'xml':
        resp = make_response(branch.map_xml, 200)
        resp.mimetype = "text/xml"
        return resp
    elif feature == 'repository-path':
        return ''
    elif feature == 'connection-string':
        return branch.conn_string
    else:
        abort(400, 'Invalid feature param')


@app.route('/layers/<layer_id>/<branch_id>/map', methods=['POST'])
def set_map(layer_id, branch_id):
    """http://wiki.yandex-team.ru/jandekskarty/development/fordevelopers/renderer/gui/KartypoHTTP"""
    app.logger.info('Map posted, layer: %s branch: %s', layer_id, branch_id)
    try:
        branch = \
            get_branch(layer_id, branch_id) if \
            branch_exists(layer_id, branch_id) else \
            create_branch(layer_id, branch_id)
        branch.map_xml = request.form['xml']
    except ValueError:
        app.logger.exception('Exception while saving map: ')
        abort(400, 'Invalid map contents')

    restart_services()
    return 'OK'


def source_version():
    try:
        with open('/etc/image_revision') as f:
            return f.read().strip()
    except Exception:
        return "unknown"


@app.route('/layers')
def layer_info():
    return render_template('layers.html',
                           layers=g_layers,
                           source_version=source_version(),
                           style2_maps=g_style2_maps)


@app.route('/layers/<layer_id>/reset', methods=['POST'])
def reset_map(layer_id):
    branch_id = request.values['branch'] + '_' + source_version()
    src_path = os.path.join(INIT_PATH, layer_id)
    if not os.path.exists(os.path.join(src_path, 'map.xml')) or \
       not os.path.exists(os.path.join(src_path, 'config.xml')):
        abort(404, 'layer %s not found' % layer_id)

    app.logger.info('Resetting layer: %s branch: %s', layer_id, branch_id)
    layer = g_layers.setdefault(layer_id, Layer(layer_id))
    layer.reset(branch_id)
    restart_services()

    return redirect(url_for('layer_info'))


@app.route('/layers/<layer_id>/remove', methods=['POST'])
def remove_map(layer_id):
    branch_id = request.values['branch']
    check_branch(layer_id, branch_id)
    g_layers[layer_id].remove(branch_id)

    return redirect(url_for('layer_info'))


@app.route('/layers/<layer_id>/copy', methods=['POST'])
def copy_map(layer_id):
    src_branch_id = request.values['src-branch']
    dst_branch_id = request.values['dst-branch']
    check_branch(layer_id, src_branch_id)
    g_layers[layer_id].copy(src_branch_id, dst_branch_id)

    return redirect(url_for('layer_info'))


@app.route('/layers/<layer_id>/activate', methods=['POST'])
def activate_map(layer_id):
    branch_id = request.values['branch']
    app.logger.info('Activating layer: %s branch: %s', layer_id, branch_id)
    check_branch(layer_id, branch_id)
    g_layers[layer_id].activate(branch_id)
    restart_services()

    return redirect(url_for('layer_info'))


@app.route('/layers2/map', methods=['POST'])
def upload_style2_layer():

    filenames = [
        (MAP_DESIGN_FILE,      MPMAP_DESIGN_FILE),
        ('icons.tar',          'icons.tar'),
        ('revision.json',      'revision.json')
    ]

    missing_files = [file[0] for file in filenames if file[0] not in request.files]
    if missing_files:
        return make_response(
            'Files: "{0}" are missing'.format(','.join(missing_files)),
            400)

    if SKL_DESIGN_FILE in request.files:
        filenames.append((SKL_DESIGN_FILE, MPSKL_DESIGN_FILE))

    for src_filename, dst_filename in filenames:
        src = request.files[src_filename]
        dst_path = os.path.join(LAYERS_PATH_V2, dst_filename)
        with open(dst_path, 'wb') as dst:
            ext = os.path.splitext(src_filename)[1]
            # pretty dump json files
            if ext == '.json':
                json.dump(json.loads(src.read()), dst, indent=4)
            else:
                dst.write(src.read())

    if SKL_DESIGN_FILE not in request.files:
        shutil.copy(
            os.path.join(LAYERS_PATH_V2, MPMAP_DESIGN_FILE),
            os.path.join(LAYERS_PATH_V2, MPSKL_DESIGN_FILE))

    resp = requests.post(RELOAD_DESIGN_URL, headers={'host': RENDERER_HOST})
    if not resp.ok:
        return make_response('Reload design fail: {}'.format(resp.text), 500)

    g_style2_maps[STYLE2_TESTING].update()

    return make_response('', 201)


@app.route('/layers2/revisions')
def style2_revisions():
    revisions = {}
    for stage, layer in six.iteritems(g_style2_maps):
        revisions[stage] = layer.revision()
    return make_response(json.dumps(revisions), 200)


@app.route('/ping')
def ping():
    return make_response('', 200)


def init_layers():
    try:
        for layer_id in os.listdir(BRANCHES_PATH):
            g_layers[layer_id] = Layer(layer_id)
    except:
        logging.warn('Invalid ' + BRANCHES_PATH)
        pass


def init_style2_map(branch_name, path):
    try:
        g_style2_maps[branch_name] = Style2Map(path)
    except:
        logging.warn('Invalid style2 map, branch = {0}, path = {1}'.format(branch_name, path))
        pass


def init_style2_maps():
    if not os.path.exists(INIT_PATH_V2):
        logging.warn('Invalid init style2 maps, dir "{}" not exists'.format(INIT_PATH_V2))
        return

    if not os.path.exists(LAYERS_PATH_V2):
        os.makedirs(LAYERS_PATH_V2)

    # update layers information
    files = ['groups.yaml', 'layers.yaml']
    for f in files:
        src = os.path.join(INIT_PATH_V2, f)
        dst = os.path.join(LAYERS_PATH_V2, f)
        if os.path.exists(src):
            shutil.copyfile(src, dst)

    # first initialization of style2 map, no override
    style2_map_files = [MPMAP_DESIGN_FILE, MPSKL_DESIGN_FILE, 'icons.tar', 'revision.json']
    for f in style2_map_files:
        src = os.path.join(INIT_PATH_V2, f)
        dst = os.path.join(LAYERS_PATH_V2, f)
        if os.path.exists(src) and not os.path.exists(dst):
            shutil.copyfile(src, dst)

    init_style2_map(STYLE2_PRODUCTION, INIT_PATH_V2)
    init_style2_map(STYLE2_TESTING, LAYERS_PATH_V2)


setup_logging()
logging.info('Starting...')
init_layers()
init_style2_maps()
