import datetime
import json
import logging
import os
import requests
from apscheduler.schedulers.background import BackgroundScheduler
from dateutil.parser import parser
from flask import Flask
from flask import jsonify
from flask import render_template
from flask_sqlalchemy import SQLAlchemy
from math import atan2
from math import cos
from math import radians
from math import sin
from math import sqrt

from config import app_key
from config import application
from config import bing_api_key
from config import cluster
from config import config_app
from config import devices
from config import gateway_locations
from config import path_db
from config import refresh_period_seconds
from config import start_lat
from config import start_lon

logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
logger = logging.getLogger(__name__)

app = config_app(Flask(__name__, template_folder="./templates"))
db = SQLAlchemy(app)


def schedule_get_new_data():
    get_new_data()


scheduler = BackgroundScheduler()
job = scheduler.add_job(schedule_get_new_data, 'interval', days=1)
scheduler.start()


class Location(db.Model):
    __tablename__ = "location"

    id = db.Column(db.Integer, primary_key=True)
    added_at = db.Column(db.DateTime, default=datetime.datetime.now)
    device_id = db.Column(db.String(250))
    raw = db.Column(db.String(250))
    datetime = db.Column(db.String(250))
    datetime_obj = db.Column(db.DateTime)
    latitude = db.Column(db.String(250))
    longitude = db.Column(db.String(250))
    altitude = db.Column(db.Integer)
    hdop = db.Column(db.Float)

    def __repr__(self):
        return '<ID %r>' % self.id

    @property
    def serialize(self):
        """Return object data in easily serializeable format"""
        return {
            'device_id': self.device_id,
            'datetime': self.datetime,
            'latitude': self.latitude,
            'longitude': self.longitude,
            'altitude': self.altitude,
            'hdop': self.hdop
        }


class LastAcquisition(db.Model):
    __tablename__ = "last_data"

    id = db.Column(db.Integer, primary_key=True)
    last_datetime = db.Column(db.DateTime)

    def __repr__(self):
        return '<ID %r>' % self.id


if not os.path.exists(path_db):
    db.create_all()


@app.route('/map')
def main_page():
    get_new_data()
    return render_template('map.html',
                           bing_api_key=bing_api_key,
                           devices=devices,
                           gateway_locations=gateway_locations,
                           location_data=Location.query.all(),
                           refresh_period_seconds=refresh_period_seconds,
                           start_lat=start_lat,
                           start_lon=start_lon)


@app.route('/past/<seconds>')
def get_past_data(seconds):
    if seconds_from_last() > 10:
        get_new_data()

    if seconds == '0':
        markers = Location.query.all()
    else:
        past_dt_object = datetime.datetime.now() - datetime.timedelta(seconds=int(seconds))
        markers = Location.query.filter(Location.added_at > past_dt_object).all()
    return jsonify([i.serialize for i in markers])


def get_new_data():
    last_seconds = seconds_from_last()
    if last_seconds:
        past_seconds = int(last_seconds) + 1
    else:
        past_seconds = 604800  # 7 days, max The Things Network storage allows

    for each_device in devices:
        endpoint = "https://{cluster_loc}.cloud.thethings.network/api/v3/as/applications/{app}/devices/{dev}/packages/storage/uplink_message?order=-received_at&type=uplink_message?last={time}".format(
            cluster_loc=cluster, app=application, dev=each_device, time="{}s".format(past_seconds))
        logger.info(endpoint)
        key = 'Bearer {}'.format(app_key)
        headers = {'Accept': 'text/event-stream', 'Authorization': key}
        response = requests.get(endpoint, headers=headers)
        if response.status_code != 200:
            logger.info(response.reason)
        try:
            response_format = "{\"data\": [" + response.text.replace("\n\n", ",")[:-1] + "]}"
            response_data = json.loads(response_format)
            uplink_msg = response_data["data"]
            for each_resp in uplink_msg:
                response_data = each_resp["result"]
                uplink_message = response_data["uplink_message"]

                received = response_data["received_at"]
                lat = uplink_message["decoded_payload"].get("latitude", "")
                lon = uplink_message["decoded_payload"].get("longitude", "")
                alt = uplink_message["decoded_payload"].get("altitude", "")
                qos = uplink_message["decoded_payload"].get("hdop", "")
                end_device_ids = response_data["end_device_ids"]
                device = end_device_ids["device_id"]
                rawpay = uplink_message["frm_payload"]

                if (not Location.query.filter(Location.datetime == received).first() and
                        -90 < float(lat) <= 90 and -120 <= float(lon) <= 80):
                    logger.info("{}, {}".format(lat, lon))
                    new_location = Location(
                        device_id=device,
                        raw=rawpay,
                        datetime_obj=parser().parse(received),
                        datetime=received,
                        latitude=lat,
                        longitude=lon,
                        altitude=alt,
                        hdop=qos)
                    db.session.add(new_location)
                    db.session.commit()
                    logger.info(new_location)
        except:
            pass

    set_date_now()


def set_date_now():
    date_last = LastAcquisition.query.first()
    if not date_last:
        new_last = LastAcquisition(last_datetime=datetime.datetime.now())
        db.session.add(new_last)
        db.session.commit()
    else:
        date_last.last_datetime = datetime.datetime.now()
        db.session.commit()


def seconds_from_last():
    date_last = LastAcquisition.query.first()
    if date_last:
        return (datetime.datetime.now() - date_last.last_datetime).total_seconds()


def distance_coordinates(lat1, lon1, lat2, lon2):
    # approximate radius of earth in km
    R = 6373.0

    lat1 = radians(lat1)
    lon1 = radians(lon1)
    lat2 = radians(lat2)
    lon2 = radians(lon2)

    dlon = lon2 - lon1
    dlat = lat2 - lat1

    a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
    c = 2 * atan2(sqrt(a), sqrt(1 - a))

    distance = R * c

    return distance  # km


if __name__ == '__main__':
    db.create_all()
    app.run(debug=True, host='0.0.0.0', port=8000)