from datetime import datetime, timedelta, timezone

from flask import Blueprint, render_template, Response, flash, redirect, url_for, request
from sqlalchemy import exc, desc, or_

from app.extensions import db
from app.models import AbstractResource
from app.models.bridges import BridgeConf, Bridge
from app.models.alarms import Alarm
from app import Origin, Proxy
from app.models.base import Group, MirrorList
from app.portal.forms import EditGroupForm, NewGroupForm, NewOriginForm, EditOriginForm, LifecycleForm, \
    NewBridgeConfForm, EditBridgeConfForm, NewMirrorListForm

portal = Blueprint("portal", __name__, template_folder="templates", static_folder="static")


@portal.app_template_filter("mirror_expiry")
def calculate_mirror_expiry(s: datetime) -> str:
    expiry = s + timedelta(days=3)
    countdown = expiry - datetime.utcnow()
    if countdown.days == 0:
        return f"{countdown.seconds // 3600} hours"
    return f"{countdown.days} days"


@portal.app_template_filter("format_datetime")
def format_datetime(s: datetime) -> str:
    if s is None:
        return "Unknown"
    return s.strftime("%a, %d %b %Y %H:%M:%S")


@portal.route("/")
def portal_home():
    groups = Group.query.order_by(Group.group_name).all()
    now = datetime.now(timezone.utc)
    proxies = Proxy.query.filter(Proxy.destroyed == None).all()
    last24 = len(Proxy.query.filter(Proxy.deprecated > (now - timedelta(days=1))).all())
    last72 = len(Proxy.query.filter(Proxy.deprecated > (now - timedelta(days=3))).all())
    lastweek = len(Proxy.query.filter(Proxy.deprecated > (now - timedelta(days=7))).all())
    return render_template("home.html.j2", section="home", groups=groups, last24=last24, last72=last72,
                           lastweek=lastweek, proxies=proxies)


@portal.route("/groups")
def view_groups():
    groups = Group.query.order_by(Group.group_name).all()
    return render_template("groups.html.j2", section="group", groups=groups)


@portal.route("/group/new", methods=['GET', 'POST'])
def new_group():
    form = NewGroupForm()
    if form.validate_on_submit():
        group = Group()
        group.group_name = form.group_name.data
        group.description = form.description.data
        group.eotk = form.eotk.data
        group.created = datetime.utcnow()
        group.updated = datetime.utcnow()
        try:
            db.session.add(group)
            db.session.commit()
            flash(f"Created new group {group.group_name}.", "success")
            return redirect(url_for("portal.edit_group", group_id=group.id))
        except exc.SQLAlchemyError as e:
            print(e)
            flash("Failed to create new group.", "danger")
            return redirect(url_for("portal.view_groups"))
    return render_template("new.html.j2", section="group", form=form)


@portal.route('/group/edit/<group_id>', methods=['GET', 'POST'])
def edit_group(group_id):
    group = Group.query.filter(Group.id == group_id).first()
    if group is None:
        return Response(render_template("error.html.j2",
                                        section="group",
                                        header="404 Group Not Found",
                                        message="The requested group could not be found."),
                        status=404)
    form = EditGroupForm(description=group.description,
                         eotk=group.eotk)
    if form.validate_on_submit():
        group.description = form.description.data
        group.eotk = form.eotk.data
        group.updated = datetime.utcnow()
        try:
            db.session.commit()
            flash("Saved changes to group.", "success")
        except exc.SQLAlchemyError:
            flash("An error occurred saving the changes to the group.", "danger")
    return render_template("group.html.j2",
                           section="group",
                           group=group, form=form)


@portal.route("/origin/new", methods=['GET', 'POST'])
@portal.route("/origin/new/<group_id>", methods=['GET', 'POST'])
def new_origin(group_id=None):
    form = NewOriginForm()
    form.group.choices = [(x.id, x.group_name) for x in Group.query.all()]
    if form.validate_on_submit():
        origin = Origin()
        origin.group_id = form.group.data
        origin.domain_name = form.domain_name.data
        origin.description = form.description.data
        origin.auto_rotation = form.auto_rotate.data
        origin.created = datetime.utcnow()
        origin.updated = datetime.utcnow()
        try:
            db.session.add(origin)
            db.session.commit()
            flash(f"Created new origin {origin.domain_name}.", "success")
            return redirect(url_for("portal.edit_origin", origin_id=origin.id))
        except exc.SQLAlchemyError as e:
            print(e)
            flash("Failed to create new origin.", "danger")
            return redirect(url_for("portal.view_origins"))
    if group_id:
        form.group.data = group_id
    return render_template("new.html.j2", section="origin", form=form)


@portal.route('/origin/edit/<origin_id>', methods=['GET', 'POST'])
def edit_origin(origin_id):
    origin = Origin.query.filter(Origin.id == origin_id).first()
    if origin is None:
        return Response(render_template("error.html.j2",
                                        section="origin",
                                        header="404 Origin Not Found",
                                        message="The requested origin could not be found."),
                        status=404)
    form = EditOriginForm(group=origin.group_id,
                          description=origin.description)
    form.group.choices = [(x.id, x.group_name) for x in Group.query.all()]
    if form.validate_on_submit():
        origin.group_id = form.group.data
        origin.description = form.description.data
        origin.auto_rotation = form.auto_rotate.data
        origin.updated = datetime.utcnow()
        try:
            db.session.commit()
            flash("Saved changes to group.", "success")
        except exc.SQLAlchemyError:
            flash("An error occurred saving the changes to the group.", "danger")
    return render_template("origin.html.j2",
                           section="origin",
                           origin=origin, form=form)


@portal.route("/origins")
def view_origins():
    origins = Origin.query.order_by(Origin.domain_name).all()
    return render_template("origins.html.j2", section="origin", origins=origins)


@portal.route("/proxies")
def view_proxies():
    proxies = Proxy.query.filter(Proxy.destroyed == None).order_by(desc(Proxy.updated)).all()
    return render_template("proxies.html.j2", section="proxy", proxies=proxies)


@portal.route("/proxy/block/<proxy_id>", methods=['GET', 'POST'])
def blocked_proxy(proxy_id):
    proxy = Proxy.query.filter(Proxy.id == proxy_id, Proxy.destroyed == None).first()
    if proxy is None:
        return Response(render_template("error.html.j2",
                                        header="404 Proxy Not Found",
                                        message="The requested proxy could not be found."))
    form = LifecycleForm()
    if form.validate_on_submit():
        proxy.deprecate(reason="manual")
        db.session.commit()
        flash("Proxy will be shortly replaced.", "success")
        return redirect(url_for("portal.edit_origin", origin_id=proxy.origin.id))
    return render_template("lifecycle.html.j2",
                           header=f"Mark proxy for {proxy.origin.domain_name} as blocked?",
                           message=proxy.url,
                           section="proxy",
                           form=form)


@portal.route("/search")
def search():
    query = request.args.get("query")
    proxies = Proxy.query.filter(or_(Proxy.url.contains(query)), Proxy.destroyed == None).all()
    origins = Origin.query.filter(or_(Origin.description.contains(query), Origin.domain_name.contains(query))).all()
    return render_template("search.html.j2", section="home", proxies=proxies, origins=origins)


@portal.route('/alarms')
def view_alarms():
    three_days_ago = datetime.now(timezone.utc) - timedelta(days=3)
    alarms = Alarm.query.filter(Alarm.last_updated >= three_days_ago).order_by(
        Alarm.alarm_state, desc(Alarm.state_changed)).all()
    return render_template("alarms.html.j2", section="alarm", alarms=alarms)


@portal.route('/lists')
def view_mirror_lists():
    mirrorlists = MirrorList.query.filter(MirrorList.destroyed == None).all()
    return render_template("mirrorlists.html.j2", section="list", mirrorlists=mirrorlists)


@portal.route("/list/destroy/<list_id>")
def destroy_mirror_list(list_id):
    return "not implemented"


@portal.route("/list/new", methods=['GET', 'POST'])
@portal.route("/list/new/<group_id>", methods=['GET', 'POST'])
def new_mirror_list(group_id=None):
    form = NewMirrorListForm()
    form.provider.choices = [
        ("github", "GitHub"),
        ("gitlab", "GitLab"),
        ("s3", "AWS S3"),
    ]
    form.format.choices = [
        ("bc2", "Bypass Censorship v2"),
        ("bc3", "Bypass Censorship v3"),
        ("bca", "Bypass Censorship Analytics"),
        ("bridgelines", "Tor Bridge Lines")
    ]
    form.container.description = "GitHub Project, GitLab Project or AWS S3 bucket name."
    form.branch.description = "Ignored for AWS S3."
    if form.validate_on_submit():
        mirror_list = MirrorList()
        mirror_list.provider = form.provider.data
        mirror_list.format = form.format.data
        mirror_list.description = form.description.data
        mirror_list.container = form.container.data
        mirror_list.branch = form.branch.data
        mirror_list.filename = form.filename.data
        mirror_list.created = datetime.utcnow()
        mirror_list.updated = datetime.utcnow()
        try:
            db.session.add(mirror_list)
            db.session.commit()
            flash(f"Created new mirror list.", "success")
            return redirect(url_for("portal.view_mirror_lists"))
        except exc.SQLAlchemyError as e:
            print(e)
            flash("Failed to create new mirror list.", "danger")
            return redirect(url_for("portal.view_mirror_lists"))
    if group_id:
        form.group.data = group_id
    return render_template("new.html.j2", section="list", form=form)


@portal.route("/bridgeconfs")
def view_bridgeconfs():
    bridgeconfs = BridgeConf.query.filter(BridgeConf.destroyed == None).all()
    return render_template("bridgeconfs.html.j2", section="bridgeconf", bridgeconfs=bridgeconfs)


@portal.route("/bridgeconf/new", methods=['GET', 'POST'])
@portal.route("/bridgeconf/new/<group_id>", methods=['GET', 'POST'])
def new_bridgeconf(group_id=None):
    form = NewBridgeConfForm()
    form.group.choices = [(x.id, x.group_name) for x in Group.query.all()]
    form.provider.choices = [
        ("aws", "AWS Lightsail"),
        ("hcloud", "Hetzner Cloud"),
        ("ovh", "OVH Public Cloud"),
        ("gandi", "GandiCloud VPS")
    ]
    form.method.choices = [
        ("any", "Any (BridgeDB)"),
        ("email", "E-Mail (BridgeDB)"),
        ("moat", "Moat (BridgeDB)"),
        ("https", "HTTPS (BridgeDB)"),
        ("none", "None (Private)")
    ]
    if form.validate_on_submit():
        bridge_conf = BridgeConf()
        bridge_conf.group_id = form.group.data
        bridge_conf.provider = form.provider.data
        bridge_conf.method = form.method.data
        bridge_conf.description = form.description.data
        bridge_conf.number = form.number.data
        bridge_conf.created = datetime.utcnow()
        bridge_conf.updated = datetime.utcnow()
        try:
            db.session.add(bridge_conf)
            db.session.commit()
            flash(f"Created new bridge configuration {bridge_conf.id}.", "success")
            return redirect(url_for("portal.view_bridgeconfs"))
        except exc.SQLAlchemyError as e:
            print(e)
            flash("Failed to create new bridge configuration.", "danger")
            return redirect(url_for("portal.view_bridgeconfs"))
    if group_id:
        form.group.data = group_id
    return render_template("new.html.j2", section="bridgeconf", form=form)


@portal.route("/bridges")
def view_bridges():
    bridges = Bridge.query.filter(Bridge.destroyed == None).all()
    return render_template("bridges.html.j2", section="bridge", bridges=bridges)


@portal.route('/bridgeconf/edit/<bridgeconf_id>', methods=['GET', 'POST'])
def edit_bridgeconf(bridgeconf_id):
    bridgeconf = BridgeConf.query.filter(BridgeConf.id == bridgeconf_id).first()
    if bridgeconf is None:
        return Response(render_template("error.html.j2",
                                        section="bridge",
                                        header="404 Bridge Configuration Not Found",
                                        message="The requested bridge configuration could not be found."),
                        status=404)
    form = EditBridgeConfForm(description=bridgeconf.description,
                              number=bridgeconf.number)
    if form.validate_on_submit():
        bridgeconf.description = form.description.data
        bridgeconf.number = form.number.data
        bridgeconf.updated = datetime.utcnow()
        try:
            db.session.commit()
            flash("Saved changes to bridge configuration.", "success")
        except exc.SQLAlchemyError:
            flash("An error occurred saving the changes to the bridge configuration.", "danger")
    return render_template("bridgeconf.html.j2",
                           section="bridgeconf",
                           bridgeconf=bridgeconf, form=form)


@portal.route("/bridge/block/<bridge_id>", methods=['GET', 'POST'])
def blocked_bridge(bridge_id):
    bridge: Bridge = Bridge.query.filter(Bridge.id == bridge_id, Bridge.destroyed == None).first()
    if bridge is None:
        return Response(render_template("error.html.j2",
                                        header="404 Proxy Not Found",
                                        message="The requested bridge could not be found."))
    form = LifecycleForm()
    if form.validate_on_submit():
        bridge.deprecate(reason="manual")
        db.session.commit()
        flash("Bridge will be shortly replaced.", "success")
        return redirect(url_for("portal.edit_bridgeconf", bridgeconf_id=bridge.conf_id))
    return render_template("lifecycle.html.j2",
                           header=f"Mark bridge {bridge.hashed_fingerprint} as blocked?",
                           message=bridge.hashed_fingerprint,
                           section="bridge",
                           form=form)


def response_404(message: str):
    return Response(render_template("error.html.j2",
                                    header="404 Not Found",
                                    message=message))


def view_lifecycle(*,
                   header: str,
                   message: str,
                   success_message: str,
                   success_view: str,
                   section: str,
                   resource: AbstractResource,
                   action: str):
    form = LifecycleForm()
    if form.validate_on_submit():
        if action == "destroy":
            resource.destroy()
        elif action == "deprecate":
            resource.deprecate(reason="manual")
        else:
            flash("Unknown action")
            return redirect(url_for("portal.portal_home"))
        db.session.commit()
        flash(success_message, "success")
        return redirect(url_for(success_view))
    return render_template("lifecycle.html.j2",
                           header=header,
                           message=message,
                           section=section,
                           form=form)


@portal.route("/bridgeconf/destroy/<bridgeconf_id>", methods=['GET', 'POST'])
def destroy_bridgeconf(bridgeconf_id: int):
    bridgeconf = BridgeConf.query.filter(BridgeConf.id == bridgeconf_id, BridgeConf.destroyed == None).first()
    if bridgeconf is None:
        return response_404("The requested bridge configuration could not be found.")
    return view_lifecycle(
        header=f"Destroy bridge configuration?",
        message=bridgeconf.description,
        success_view="portal.view_bridgeconfs",
        success_message="All bridges from the destroyed configuration will shortly be destroyed at their providers.",
        section="bridgeconf",
        resource=bridgeconf,
        action="destroy"
    )


@portal.route("/origin/destroy/<origin_id>", methods=['GET', 'POST'])
def destroy_origin(origin_id: int):
    origin = Origin.query.filter(Origin.id == origin_id, Origin.destroyed == None).first()
    if origin is None:
        return response_404("The requested origin could not be found.")
    return view_lifecycle(
        header=f"Destroy origin {origin.domain_name}",
        message=origin.description,
        success_message="All proxies from the destroyed origin will shortly be destroyed at their providers.",
        success_view="portal.view_origins",
        section="origin",
        resource=origin,
        action="destroy"
    )
