# coding=utf-8
""""""
from __future__ import absolute_import, division, print_function, \
    unicode_literals

import logging
import socket
from collections import OrderedDict, namedtuple

import redis
import six
import sqlalchemy as sa
import sqlalchemy.dialects
import sqlalchemy.exc
from flask import current_app, flash, redirect, \
    render_template, request, session, url_for
from pathlib import Path
from six import text_type

from abilian.core.commands import config as cmd_config
from abilian.core.extensions import csrf, db
from abilian.core.models.subjects import User
from abilian.services import get_service
from abilian.services.security import Admin, Anonymous
from abilian.web.blueprints import Blueprint

logger = logging.getLogger(__name__)
setup = Blueprint(
    'setup',
    __name__,
    allowed_roles=Anonymous,
    template_folder='templates',
)

# list supported dialects and detect unavailable ones due to missing dbapi
# module ('psycopg2' missing for example)

_dialects = OrderedDict((
    ('sqlite', 'SQLite (for demo)'),
    ('postgresql', 'PostgreSQL'),
))

_dialects_unavailable = OrderedDict()

for dialect, _label in _dialects.items():
    d = sa.dialects.registry.load(dialect)
    try:
        d.dbapi()
    except ImportError as e:
        _dialects_unavailable[dialect] = str(e)

# enumerate steps for left column progress
Step = namedtuple('SetupStep', ('name', 'endpoint', 'title', 'description'))

_setup_steps = (
    Step('db', 'step_db', 'Setup Database', 'Setup basic database connection'),
    Step('redis', 'step_redis', 'Setup Redis', 'Redis connection'),
    Step(
        'site_info',
        'step_site_info',
        'Basic site informations',
        'Site name, admin email...',
    ),
    Step('admin_account', 'step_admin_account', 'Admin account', None),
    Step('finalize', 'finalize', 'Finalize', None),
)


@setup.before_request
def step_progress():
    session.permanent = False  # limit lifetime to browser session
    current_step = next_step = prev_step = None

    for step in _setup_steps:
        if current_step is not None:
            next_step = step
            break
        endpoint = '{}.{}'.format(setup.name, step.endpoint)
        if endpoint == request.endpoint:
            current_step = step
        else:
            prev_step = step

    request.setup_step_progress = {
        'current_step': current_step,
        'prev_step': prev_step,
        'next_step': next_step,
    }


@setup.context_processor
def common_context():
    ctx = {
        'setup_steps': _setup_steps,
        'dialects': _dialects,
        'dialects_unavailable': _dialects_unavailable,
        'validated_steps': session_get('validated', ()),
    }
    ctx.update(request.setup_step_progress)
    return ctx


# session helpers
def session_set(key, data):
    session.setdefault('abilian_setup', {})[key] = data


def session_get(key, default=None):
    return session.setdefault('abilian_setup', {}).get(key, default)


def session_clear():
    if 'abilian_setup' in session:
        del session['abilian_setup']


def step_validated(step_endpoint, valid=True):
    validated = set(session_get('validated', ()))
    if valid:
        validated.add(step_endpoint)
    else:
        if step_endpoint in validated:
            validated.remove(step_endpoint)

    session_set('validated', tuple(validated))


#
# DB Setup
#
@csrf.exempt
@setup.route('/', methods=['GET', 'POST'])
def step_db():
    if request.method == 'POST':
        return step_db_validate()
    return step_db_form()


def step_db_form():
    return render_template(
        'setupwizard/step_db.html',
        data=session_get('db', {}),
    )


def step_db_validate():
    form = request.form
    dialect = form['dialect']
    username = form.get('username', '').strip()
    password = form.get('password', '').strip()
    host = form.get('host', '').strip()
    port = form.get('port', '').strip()
    database = form.get('database', '').strip()

    # build db_uri
    db_uri = '{}://'.format(dialect)

    if dialect != 'sqlite':
        if username:
            db_uri += username
            # check password only if we have a username
            if password:
                db_uri += ':' + password

        # FIXME: it is an error to have a username and no host,
        # SA will interpret username:password as host:port
        if host:
            db_uri += '@' + host
            port = port
            if port:
                db_uri += ':' + port

    db_uri += '/'

    if dialect == 'sqlite' and (not database or database == ':memory:'):
        database = text_type(
            Path(current_app.instance_path) / 'data' / 'sqlite.db',
        )

    db_uri += database

    # store in session
    db_params = {
        'uri': db_uri,
        'dialect': dialect,
        'username': username,
        'password': password,
        'host': host,
        'port': port,
        'database': database,
    }
    session_set('db', db_params)

    # test connection
    engine = sa.create_engine(db_uri)
    error = None
    try:
        conn = engine.connect()
    except sa.exc.OperationalError as e:
        error = str(e)
    else:
        conn.close()
        del conn

    engine.dispose()

    # FIXME: at this point we have not tested the connection is valid, i.e, we
    # have CREATE, SELECT, UPDATE, DELETE rights on the database

    if error:
        flash(error, 'error')
        return step_db_form()

    step_validated('db')
    next_step = request.setup_step_progress['next_step']
    endpoint = '{}.{}'.format(setup.name, next_step.endpoint)
    return redirect(url_for(endpoint))


#
# Redis
#
@csrf.exempt
@setup.route('/redis', methods=['GET', 'POST'])
def step_redis():
    if request.method == 'POST':
        return step_redis_validate()
    return step_redis_form()


def step_redis_form():
    return render_template(
        'setupwizard/step_redis.html',
        data=session_get('redis', {}),
    )


def step_redis_validate():
    form = request.form
    data = dict(
        host=form.get('host', 'localhost').strip(),
        port=form.get('port', '').strip() or '6379',
        db=form.get('db', '').strip() or '1',
    )

    for k in ('port', 'db'):
        try:
            data[k] = int(data[k])
        except ValueError:
            pass

    data['uri'] = 'redis://{host}:{port}/{db}'.format(**data)
    session_set('redis', data)
    error = None

    try:
        r = redis.StrictRedis(
            host=data['host'],
            port=data['port'],
            db=data['db'],
        )
    except Exception as e:
        error = 'Connection error, check parameters'
        raise e

    try:
        r.info()
    except redis.exceptions.InvalidResponse:
        error = (
            "Connection error: doesn't look like it's a redis server. "
            "Verify host and port are those of your redis server."
        )
    except redis.exceptions.ResponseError as e:
        error = 'Redis server response: {}'.format(e)
    except redis.exceptions.RedisError as e:
        error = 'Unknown redis error ({})'.format(e)

    if error:
        flash(error, 'error')
        return step_redis_form()

    step_validated('redis')
    next_step = request.setup_step_progress['next_step']
    endpoint = '{}.{}'.format(setup.name, next_step.endpoint)
    return redirect(url_for(endpoint))


#
# Site info
#
@csrf.exempt
@setup.route('/site_info', methods=['GET', 'POST'])
def step_site_info():
    if request.method == 'POST':
        return step_site_info_validate()
    return step_site_info_form()


def get_possible_hostnames():
    hostname = socket.gethostname()
    fqdn = socket.getfqdn()
    names = {'localhost': ['127.0.0.1']}

    for name in (hostname, fqdn):
        try:
            name, aliases, ips = socket.gethostbyname_ex(name)
        except socket.error:
            continue
        names.setdefault(name, []).extend(ips)
        for a in aliases:
            names.setdefault(a, []).extend(ips)

    return sorted(
        '{} ({})'.format(name, ', '.join(sorted(set(ips))))
        for name, ips in six.iteritems(names)
    )


def step_site_info_form():
    cfg = current_app.config
    default_data = {
        'sitename': cfg.get('SITE_NAME', '') or '',
        'mailsender': cfg.get('MAIL_SENDER', '') or '',
    }
    return render_template(
        'setupwizard/step_site_info.html',
        data=session_get('site_info', default_data),
        suggested_hosts=get_possible_hostnames(),
    )


def step_site_info_validate():
    form = request.form
    data = {
        'sitename': form.get('sitename', '').strip(),
        'mailsender': form.get('mailsender', '').strip(),
        'server_mode': form.get('server_mode', '').strip(),
    }

    session_set('site_info', data)
    step_validated('site_info')
    next_step = request.setup_step_progress['next_step']
    endpoint = '{}.{}'.format(setup.name, next_step.endpoint)
    return redirect(url_for(endpoint))


#
# Admin account creation
#
@csrf.exempt
@setup.route('/admin_account', methods=['GET', 'POST'])
def step_admin_account():
    if request.method == 'POST':
        return step_admin_account_validate()
    return step_admin_account_form()


def step_admin_account_form():
    return render_template(
        'setupwizard/step_admin_account.html',
        data=session_get('admin_account', {}),
    )


def step_admin_account_validate():
    form = request.form
    password = form.get('password', '').strip()
    confirm_password = form.get('confirm_password', '').strip()
    admin_user = {
        'email': form.get('email', '').strip(),
        'name': form.get('name', '').strip(),
        'firstname': form.get('firstname', '').strip(),
        'password': password,
        'confirm_password': confirm_password,
    }
    session_set('admin_account', admin_user)

    if password != confirm_password:
        flash('Password fields don\'t match', 'error')
        return step_admin_account_form()

    step_validated('admin_account')
    next_step = request.setup_step_progress['next_step']
    endpoint = '{}.{}'.format(setup.name, next_step.endpoint)
    return redirect(url_for(endpoint))


#
# Finalize
#
@csrf.exempt
@setup.route('/finalize', methods=['GET', 'POST'])
def finalize():
    validated = session_get('validated')
    assert all(s.name in validated for s in _setup_steps[:-1])

    if request.method == 'POST':
        return finalize_validate()
    return finalize_form()


def finalize_form():
    file_location = Path(current_app.instance_path) / 'config.py'
    return render_template(
        'setupwizard/finalize.html',
        file_location=file_location,
    )


def finalize_validate():
    config_file = Path(current_app.instance_path) / 'config.py'
    logging_file = Path(current_app.instance_path) / 'logging.yml'

    assert not config_file.exists()
    config = cmd_config.DefaultConfig(logging_file='logging.yml')
    config.SQLALCHEMY_DATABASE_URI = session_get('db')['uri']

    redis_uri = session_get('redis')['uri']
    config.REDIS_URI = redis_uri
    config.BROKER_URL = redis_uri
    config.CELERY_RESULT_BACKEND = redis_uri

    d = session_get('site_info')
    config.SITE_NAME = d['sitename']
    config.MAIL_SENDER = d['mailsender']

    is_production = d['server_mode'] == 'production'
    config.PRODUCTION = is_production
    config.DEBUG = not is_production
    config.DEBUG_TB_ENABLED = config.DEBUG
    config.CELERY_ALWAYS_EAGER = not is_production

    cmd_config.write_config(config_file, config)
    cmd_config.maybe_write_logging(logging_file)

    admin_account = session_get('admin_account')
    # create a new app that will be configured with new config,
    # to create database and admin_user
    setup_app = current_app._get_current_object()
    app = setup_app.__class__(
        setup_app.import_name,
        static_url_path=setup_app.static_url_path,
        static_folder=setup_app.static_folder,
        template_folder=setup_app.template_folder,
        instance_path=setup_app.instance_path,
    )
    with app.test_request_context('/setup/finalize'):
        app.create_db()
        db_session = db.session()
        admin = User(
            email=admin_account['email'],
            password=admin_account['password'],
            last_name=admin_account['name'],
            first_name=admin_account['firstname'],
            can_login=True,
        )
        db_session.add(admin)
        security = get_service('security')
        security.grant_role(admin, Admin)
        db_session.commit()

    session_clear()

    return render_template(
        'setupwizard/done.html',
        config_file=config_file,
        logging_file=logging_file,
    )
