import os
import requests
from urllib.parse import urlparse
import rollbar
import rollbar.contrib.flask
from flask import Flask, request, make_response, session, render_template, \
    redirect, url_for, jsonify
from flask import got_request_exception
from flask.ext import assets
from flask_oauthlib.client import OAuth
from webassets.filter import get_filter

from helix.sales_force import tasks as sf_tasks
from helix.Services.doc_gen_service import DocGenService
from helix.Services.dxf_helper import DXFHelper
from helix.Services.dxf_service import DXFService
from helix.api.api import api
from helix.calculators.calculator import Calculator
from helix.constants import redis_constant, sql_constant
from helix.constants.file_validation_error import FileValidationError, FileValidationException
from helix.constants.inverter_type import InverterType
from helix.constants.system_type import SystemType
from helix.csv_builder import CsvBuilder
from helix.doc_gen_params_builder import DocGenParamsBuilder
from helix.forms.ebom_form import EbomForm, InverterBrandForm, \
    SupervisorForm, SupervisorFormSMA, StandAloneInverterFormSMA, \
    StandAloneInverterFormDelta
from helix.forms.input_form import InputForm, ArrayForm, TestDXFForm
from helix.models.dxf.dxf_error import DXFError, OldDxfFormatException
from helix.presenters.image_presenter import ImagePresenter
from helix.presenters.panel_presenter import ProjectPresenter
from helix.session_manager import SessionManager
from helix.validators.file_validator import FileValidator, FileType
from helix.validators.subarray_validator import SubarrayValidator

app = Flask(__name__)
app.register_blueprint(api, url_prefix='/api')
app.secret_key = os.getenv('SECRET_KEY', 'verysecretkey')
app.config['PROFILE'] = True


# Sales Force integrations
oauth = OAuth()
SF_BASE_URL = os.getenv('SFDC_BASE_URL', 'https://test.salesforce.com')
sales_force = oauth.remote_app('sales_force',
    consumer_key=os.getenv('SFDC_ACCESS_KEY_ID'),
    consumer_secret=os.getenv('SFDC_SECRET_ACCESS_KEY'),
    base_url=SF_BASE_URL,
    request_token_url=None, # OAuth 2
    access_token_method='POST', # Sales Force requirement
    access_token_url=SF_BASE_URL + '/services/oauth2/token',
    authorize_url=SF_BASE_URL + '/services/oauth2/authorize',
)

assets_env = assets.Environment(app)
assets_env.init_app(app)
assets_env.load_path = [
    os.path.join(os.path.dirname(__file__), 'scss')
]
sass = get_filter('scss')
sass.load_paths = [os.path.join(os.path.dirname(os.path.realpath(__file__)), 'scss')]

assets_env.register(
    'main_css',
    assets.Bundle(
        '*.scss',
        filters=(sass,),
        output='css/main.css'
    )
)


@app.before_first_request
def init_rollbar():
    # Do nothing unless Rollbar is configured
    if not os.getenv("ROLLBAR_ACCESS_TOKEN"):
        return

    rollbar.init(os.getenv("ROLLBAR_ACCESS_TOKEN"),
                 # Setup this var in heroku to distinguish errors from different envs
                 os.getenv("ROLLBAR_ENV", "development"),
                 root=os.path.dirname(os.path.realpath(__file__)),
                 allow_logging_basic_config=False)

    got_request_exception.connect(rollbar.contrib.flask.report_exception, app)


def is_sfdc_session():
    return 'SFID' in session


@app.route("/")
def index():
    return redirect(url_for('site_characterization'))


@app.route("/test_dxf/", methods=['GET', 'POST'])
def test_dxf():
    form = TestDXFForm()
    if form.validate_on_submit():
        file = request.files['dxf_upload']
        file_contents = file.read().decode('utf-8')
        db_session = sql_constant.sql_session_maker()
        session_manager = SessionManager(session, redis_constant.redis_store, db_session)
        user_values = session_manager.user_values()
        calculator = Calculator(user_values, calculate_panel_data=False)
        l_b = calculator.L_B() * 12  # convert from feet to inches
        try:
            dxf_data = DXFService().parse(file_contents, user_values.module_system_constants(),
                                          user_values.system_type(), l_b, DXFHelper(), SubarrayValidator())
            dxf_data['panels'].sort(key=lambda p: p.id)
            dxf_data['l_b'] = l_b
            if not form.show_wind_zones.data:
                dxf_data['lb_polygons'] = []
        except DXFError as error:
            form.dxf_upload.errors.append(error.message)
            dxf_data = {}
    else:
        dxf_data = {}
    dxf_data['colors'] = [
        'red',
        'orange',
        'yellow',
        'green',
        'blue',
        'purple',
    ]
    return render_template('test_dxf.html.jinja', context=dxf_data, form=form)


# wizard steps
@app.route("/site_characterization/", methods=['GET', 'POST'])
def site_characterization():
    if is_sfdc_session():
        return redirect('/summary/')

    db_session = sql_constant.sql_session_maker()
    session_manager = SessionManager(session, redis_constant.redis_store, db_session)
    site_info_form = InputForm()
    context = session_manager.context()
    context['current_step'] = 1
    context['javascripts'] = ['site_characterization']

    if site_info_form.validate_on_submit():
        session_manager.save_form_submission(request.form)
        return redirect(url_for('summary'))

    if request.method != 'POST':
        session_manager.fill_saved_values_in_form(site_info_form)
    db_session.close()
    return render_template('site_characterization.html.jinja',
                           context=context,
                           form=site_info_form)


@app.route("/summary/")
def summary():
    db_session = sql_constant.sql_session_maker()
    session_manager = SessionManager(session, redis_constant.redis_store, db_session)
    context = session_manager.context()
    context['current_step'] = 2

    if context['site_data_available']:
        context['project_name'] = session_manager.site.project_name
        user_values = session_manager.user_values()
        calculator = Calculator(user_values, calculate_panel_data=False)
        context['wind_zones'] = user_values.system_type().system_constants().wind_zones
        context['summary_table'] = calculator.summary_table()
        context['minimum_array_sizes'] = calculator.minimum_array_sizes()
        context['l_b'] = round(calculator.L_B(), 2)
        context['k_z'] = round(calculator.k_z(), 2)
        context['q_z'] = round(calculator.q_z(), 2)
        context['warning_messages'] = set()
        context['javascripts'] = ['https://cdn.rawgit.com/noelboss/featherlight/1.7.6/release/featherlight.min.js']
        for panel_type, values in context['summary_table'].items():
            for panel_type_warnings in values['warnings']:
                for warning in panel_type_warnings:
                    context['warning_messages'].add(warning)
    else:
        context['no_proceed'] = True

    if is_sfdc_session():
        context['hide_back'] = True

    db_session.close()
    return render_template('site_summary.html.jinja', context=context)


def handle_dxf_file(session_manager, file_contents, filename=None):
    errors = []
    user_values = session_manager.user_values()
    validator = FileValidator(user_values)
    calculator = Calculator(user_values, calculate_panel_data=False)

    extension = os.path.splitext(filename)[1] or 'dxf'
    extension = extension.split('.')[-1]

    validation_error = validator.validate(file_contents, FileType.AuroraDxf, extension=extension)
    if validation_error:
        error_msg = validation_error.format_error_message()
        errors.append(error_msg)
    else:
        try:
            module_constants = user_values.module_system_constants()
            # FIXME: parsing a file with many entities is very slow
            dxf_data = DXFService().parse(file_contents,
                                          module_constants,
                                          user_values.system_type(),
                                          calculator.L_B() * 12,
                                          DXFHelper(),
                                          SubarrayValidator())
            csv = CsvBuilder().build_cad_output(dxf_data['panels'])
            session_manager.save_uploaded_file(csv, dxf_file_name=filename)

            buildings = dxf_data['buildings']
            session_manager.save_buildings_polygons(buildings)
            session_manager.save_is_drawing_inaccurate(dxf_data['is_panel_drawing_inaccurate'])

            return True, errors
        except DXFError as error:
            errors.append(error.message)
        except OldDxfFormatException as error:
            errors.append(error.message)
    return False, errors


@app.route("/array_summary/", methods=['GET', 'POST'])
def array_summary():
    """This endpoint allows you to upload a file.
    The content of the file is parsed, and then
    several objects are created that aid in the validation
    of the uploaded file.
    """

    db_session = sql_constant.sql_session_maker()
    session_manager = SessionManager(session, redis_constant.redis_store, db_session)
    context = session_manager.context()
    context['current_step'] = 3
    array_form = ArrayForm()
    if array_form.validate_on_submit():
        if 'file_upload' in request.files and request.files['file_upload'].filename:
            validator = FileValidator(session_manager.user_values())
            file = request.files['file_upload']
            file_contents = validator.obtain_stream(file)
            validation_error = validator.validate(file_contents, FileType.Csv, file)
            if not validation_error:
                session_manager.save_uploaded_file(file_contents,
                                                   cad_file_name=file.filename)
                session_manager.save_buildings_polygons([]) # no buildings in the csv file
                session_manager.save_is_drawing_inaccurate(False)
                return redirect(url_for('array_summary'))
            else:
                array_form.file_upload.errors.append(validation_error.format_error_message())
        elif 'dxf_upload' in request.files and request.files['dxf_upload'].filename:
            file = request.files['dxf_upload']
            user_values = session_manager.user_values()
            validator = FileValidator(user_values)
            file_contents = validator.obtain_stream(file)
            success, errors = handle_dxf_file(session_manager, file_contents, filename=file.filename)
            if success:
                return redirect(url_for('array_summary'))
            else:
                array_form.dxf_upload.errors.extend(errors)
        elif context['csv_available']:
            return redirect(url_for('power_station_configuration'))
        else:
            array_form.file_upload.errors.append('Please provide a .txt file!')

    context['javascripts'] = ['array_summary_bundle']
    context['hide_submit'] = True
    context['cad_file_name'] = ''
    context['dxf_file_name'] = ''
    if context['site_data_available'] and context['csv_available']:
        user_values = session_manager.user_values()
        try:
            calculator = Calculator(user_values)
            system_type = user_values.system_type()
            module_type = user_values.module_type()
            project_presenter = ProjectPresenter(system_type, module_type)

            context['wind_zones'] = system_type.system_constants().wind_zones
            context['summary_table'] = calculator.summary_table()
            context['minimum_array_sizes'] = calculator.minimum_array_sizes()
            context['seismic_anchors'] = calculator.subarray_summary()
            context['summary_values'] = calculator.summary_values()

            panels = calculator.get_computed_csv_columns()
            max_y = project_presenter.get_max_y(calculator.buildings_for_drawing,panels)
            context['panel_array'] = project_presenter.get_panel_data(panels,
                                                              calculator.subarrays,
                                                              max_y)
            context['buildings']  =  project_presenter.get_buildings(calculator.buildings_for_drawing, max_y)
            context['corners'] = project_presenter.get_corners(calculator.buildings)
            context['override_form'] = True
            context['cad_file_name'] = session_manager.site.cad_file_name or 'Upload System Text Data'
            context['dxf_file_name'] = session_manager.site.dxf_file_name or 'Upload System DXF'
            context['is_drawing_inaccurate'] = session_manager.user_values().is_panel_drawing_inaccurate()
            context['inaccurate_drawing_warning'] = 'The subarrays in this design are not parallel to each other, \
         and the graphical representation on this page may not be accurate.'

            print("=================")
            print("CORNERS : ")
            print(context['corners'])
            print("=================")

        except FileValidationException as error:
            # when calculator is about to enter infinte loop
            # it throws an exception - it is supplied wrong data
            context['site_data_available'] = False
            context['csv_available'] = False
            context['no_proceed'] = True
            context['cad_file_name'] = ''
            context['dxf_file_name'] = ''
            context['infinite_loop_detection_message'] = error.message

    elif not context['site_data_available']:
        context['no_proceed'] = True

    if is_sfdc_session() and 'dxf_link_loaded' not in session:
        context['javascripts'].append('auto_dxf_load')

    db_session.close()
    return render_template('array_summary.html.jinja', context=context, form=array_form)


@app.route("/load_dxf/", methods=['GET', 'POST'])
def load_dxf_file():
    if 'dxf_link' not in session:
        errors = ['DXF link not found']
        response = jsonify({'errors': errors})
        response.status_code = 404
        return response

    dxf_link = session['dxf_link']
    response = requests.get(dxf_link)
    if response.status_code == 200:
        file_contents = response.content.decode('utf-8')
        filename = urlparse(dxf_link).path.strip('/')

        db_session = sql_constant.sql_session_maker()
        session_manager = SessionManager(session, redis_constant.redis_store, db_session)

        success, errors = handle_dxf_file(session_manager, file_contents, filename=filename)
        session['dxf_link_loaded'] = True
        if success:
            return jsonify({'status': 'success'})
    else:
        errors = ['Unable to download DXF file from Sales Force ({})'.format(response.status_code)]

    response = jsonify({'errors': errors})
    response.status_code = 400
    return response


@app.route("/power_station_configuration/", methods=['GET', 'POST'])
def power_station_configuration():
    db_session = sql_constant.sql_session_maker()
    session_manager = SessionManager(session, redis_constant.redis_store, db_session)
    context = session_manager.context()

    if session_manager.site:
        system_type = session_manager.user_values().system_type()
    else:
        system_type = None

    inverter_brand_form = InverterBrandForm()
    if request.method != 'POST' or request.form['form_id'] != 'inverter_brand_form':
        inverter_brand_form.populate_choices(context['inverter_brands'])
    is_delta = inverter_brand_form.is_delta()

    if is_delta:
        ebom_form = EbomForm()
        standalone_inverter_form = StandAloneInverterFormDelta()
        standalone_inverter_form.populate_choices()
        supervisor_form = SupervisorForm()
    else:
        ebom_form = EbomForm()
        ebom_form.update_inverter_strings_choices(system_type)
        standalone_inverter_form = StandAloneInverterFormSMA()
        standalone_inverter_form.update_inverter_strings_choices(system_type)
        standalone_inverter_form.populate_choices(context['power_stations'], context['standalone_inverters'])
        supervisor_form = SupervisorFormSMA()
        supervisor_form.populate_choices(context['power_stations'], context['power_monitors'])

    if request.method == 'POST':
        if request.form['form_id'] == 'inverter_brand_form' and inverter_brand_form.validate_on_submit():
            session_manager.delete_power_station_config_data()
            session_manager.save_or_update_inverter_brands(request.form)
            return redirect("/power_station_configuration/")

        elif request.form['form_id'] == 'power_station_form' and ebom_form.validate_on_submit():
            session_manager.save_or_update_power_station(request.form)
            return redirect("/power_station_configuration/")

        elif request.form['form_id'] == 'standalone_inverter_form' and standalone_inverter_form.validate_on_submit():
            session_manager.save_or_update_standalone_inverter(request.form)
            return redirect("/power_station_configuration/")

        elif request.form['form_id'] == 'supervisor_form' and supervisor_form.validate_on_submit():
            session_manager.save_or_update_supervisor_monitor(request.form)
            return redirect("/power_station_configuration")

    ebom_form.power_station_description.data = "Power Station " + str(len(context['power_stations']) + 1)

    inverter_enum = InverterType.DELTA if is_delta else InverterType.SMA
    string_limits = {}
    string_defaults = {}
    for i_e in inverter_enum.all():
        string_limits[i_e.value] = list(i_e.valid_string_ranges) if i_e.valid_string_ranges is not None else []
        string_defaults[i_e.value] = i_e.default_string

    context['standalone_inverter_string_limits'] = string_limits
    context['standalone_inverter_string_defaults'] = string_defaults
    context['current_step'] = 4
    context['javascripts'] = ['power_station_configuration']

    db_session.close()
    return render_template('power_station_configuration.html.jinja',
                           context=context,
                           ebom_form=ebom_form,
                           is_delta=is_delta,
                           inverter_brand_form=inverter_brand_form,
                           standalone_inverter_form=standalone_inverter_form,
                           supervisor_monitor_form=supervisor_form)


@app.route("/download/")
def download():
    db_session = sql_constant.sql_session_maker()
    session_manager = SessionManager(session, redis_constant.redis_store, db_session)
    context = session_manager.context()
    context['current_step'] = 5
    db_session.close()
    return render_template('download.html.jinja', context=context)


@app.route("/delete_power_station/<uuid>")
def delete_power_station(uuid):
    db_session = sql_constant.sql_session_maker()
    session_manager = SessionManager(session, redis_constant.redis_store, db_session)
    session_manager.delete_power_station(uuid)
    db_session.close()
    return redirect(url_for('power_station_configuration'))


@app.route('/delete_standalone_inverter/<uuid>')
def delete_standalone_inverter(uuid):
    db_session = sql_constant.sql_session_maker()
    session_manager = SessionManager(session, redis_constant.redis_store, db_session)
    session_manager.delete_standalone_inverter(uuid)
    db_session.close()
    return redirect(url_for('power_station_configuration'))


@app.route('/delete_supervisor_monitor/<uuid>')
def delete_supervisor_monitor(uuid):
    db_session = sql_constant.sql_session_maker()
    session_manager = SessionManager(session, redis_constant.redis_store, db_session)
    session_manager.delete_supervisor_monitor(uuid)
    db_session.close()
    return redirect(url_for('power_station_configuration'))


@app.route("/result")
def result():
    db_session = sql_constant.sql_session_maker()
    session_manager = SessionManager(session, redis_constant.redis_store, db_session)
    user_values = session_manager.user_values()
    calculator = Calculator(user_values)
    csv = CsvBuilder().build_cad_output(calculator.get_computed_csv_columns())
    response = make_response(csv)
    response.headers["Content-Disposition"] = "attachment; filename=%s_result.txt" % user_values.project_name_no_spaces()
    db_session.close()
    return response


@app.route("/documentation")
def documentation():
    db_session = sql_constant.sql_session_maker()
    session_manager = SessionManager(session, redis_constant.redis_store, db_session)
    user_values = session_manager.user_values()
    calculator = Calculator(user_values)
    image_presenter = ImagePresenter(user_values.system_type(), user_values.module_type())
    doc_gen_service = DocGenService(requests, DocGenParamsBuilder(user_values, user_values.system_type(), calculator, image_presenter))
    document = doc_gen_service.generate()

    response = make_response(document)
    response.headers["Content-Disposition"] = "attachment; filename=%s_documentation.pdf" % user_values.project_name_no_spaces()
    db_session.close()
    return response


@app.route("/bom")
def bom():
    db_session = sql_constant.sql_session_maker()
    session_manager = SessionManager(session, redis_constant.redis_store, db_session)
    user_values = session_manager.user_values()
    calculator = Calculator(user_values)
    csv = CsvBuilder().build_bom_output(calculator.compute_bom())
    response = make_response(csv)
    response.headers["Content-Disposition"] = "attachment; filename=%s_bom.txt" % user_values.project_name_no_spaces()
    db_session.close()
    return response


@app.route("/exposure_categories")
def exposure_categories():
    db_session = sql_constant.sql_session_maker()
    context = SessionManager(session, redis_constant.redis_store, db_session).context()
    db_session.close()
    return render_template('exposure_categories.html.jinja', context=context)


@app.route("/helix_documentation")
def helix_documentation():
    db_session = sql_constant.sql_session_maker()
    context = SessionManager(session, redis_constant.redis_store, db_session).context()
    db_session.close()
    return render_template('helix_documentation.jinja', context=context)


# Sales Force Integration
@app.route('/sales_force_login')
def sales_force_login():
    # To test it locally: https://localhost:8443/sales_force_login?SFID=a3cL00000004QsQIAU
    sfid = request.args.get('SFID')
    if sfid:
        session.clear()
        session['SFID'] = sfid
        return sales_force.authorize(callback=url_for('sales_force_authorized', _external=True, SFID=sfid), SFID=sfid)
    else:
        return redirect('/')


@app.route('/sales_force_authorized')
def sales_force_authorized():
    next_url = url_for('summary')

    resp = sales_force.authorized_response()
    if resp is None:
        print('Unable to authenticate to SFDC.')
        return redirect(next_url)

    print('New Sales Force - OAuth2 login')
    db_session = sql_constant.sql_session_maker()
    session_manager = SessionManager(session, redis_constant.redis_store, db_session)
    session['sales_force_token'] = resp['access_token']

    data = sf_tasks.get_site_characterization_from_sales_force(session, resp['instance_url'])
    if data:
        session['dxf_link'] = data['dxf_link']
        session_manager.save_form_submission(data)
        return redirect(next_url)
    else:
        return sales_force_logout()


# FIXME
@app.route("/export-sfdc")
def export_sfdc():
    if not is_sfdc_session():
        return redirect('/')
    db_session = sql_constant.sql_session_maker()
    session_manager = SessionManager(session, redis_constant.redis_store, db_session)
    session_id = session_manager.session['id']
    data = sf_tasks.export_to_sfdc(session_id)
    db_session.close()
    return jsonify(data)
    # return redirect('/download')


@sales_force.tokengetter
def get_sales_force_token(token=None):
    return session.get('sales_force_token')


@app.route('/sales_force_logout')
def sales_force_logout():
    session.pop('SFID', None)
    session.pop('sales_force_token', None)
    session.pop('dxf_link', None)
    session.pop('dxf_link_loaded', None)
    session.clear()
    return redirect('/')
# End of Sales Force Integration


@app.template_filter('format_number')
def format_number(number):
    return "{:,g}".format(number)


@app.template_filter('is_dual_tilt')
def is_dual_tilt(system_type):
    return system_type == SystemType.dualTilt


@app.context_processor
def power_station_has_monitor():
    def _power_station_has_monitor(power_station, monitors):
        for monitor in monitors:
            if monitor['power_source'][1] == power_station['power_station_id']:
                return True
        return False
    return dict(power_station_has_monitor=_power_station_has_monitor)


@app.context_processor
def enum():
    def _enum(item):
        return enumerate(item)
    return dict(enum=_enum)


def main():
    host = '0.0.0.0'
    debug = bool(os.getenv("FLASK_DEBUG", False))
    if os.getenv('FLASK_DEBUG_SSL', None):
        port = int(os.getenv('PORT', 8443))
        app.run(host=host, port=port, debug=debug, ssl_context='adhoc')
    else:
        port = int(os.getenv('PORT', 5000))
        app.run(host=host, port=port, debug=debug)


@app.route("/fail-test")
def fail_test():
    raise RuntimeError("This is a test failure, ignore it")


if __name__ == "__main__":
    main()
