from typing import Optional, List, Tuple

import sqlalchemy
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import or_

from helpers.custom_logging import FlashLog
from helpers.time_helper import current_timestamp
from template_pwndoc import TNSTemplatePwndoc

db = SQLAlchemy()


class DbPwndocMapping(db.Model):
    __tablename__ = 'pwndoc_mapping'
    id = db.Column(db.Integer, primary_key=True)

    fid = db.Column(db.String(80), unique=True, nullable=False, index=True)
    pwndoc_id = db.Column(db.String(80), unique=True, nullable=False, index=True)

    @classmethod
    def get_pwndoc_id_from_fid(cls, fid: str) -> Optional[str]:
        res = cls.query.filter_by(fid=fid).first()
        return res.pwndoc_id if res else None

    @classmethod
    def get_fid_from_pwndoc_id(cls, pwndoc_id: str) -> Optional[str]:
        res = cls.query.filter_by(pwndoc_id=pwndoc_id).first()
        return res.fid if res else None

    @classmethod
    def save_fid_and_pwndoc_id(cls, fid: str, pwndoc_id: str):
        current_pwndoc_id: Optional[str] = cls.get_pwndoc_id_from_fid(fid)
        if current_pwndoc_id and current_pwndoc_id == pwndoc_id:
            return
        if current_pwndoc_id:
            qry = sqlalchemy.delete(cls).where(or_(cls.fid == fid, cls.pwndoc_id == pwndoc_id))
            db.session.execute(qry)

        new_mapping = DbPwndocMapping(fid=fid, pwndoc_id=pwndoc_id)
        db.session.add(new_mapping)
        db.session.commit()

    @classmethod
    def delete_all(cls):
        db.session.query(cls).delete()
        db.session.commit()


class DbTemplate(db.Model):
    __tablename__ = 'templates'

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

    fid = db.Column(db.String(80), unique=False, nullable=False, index=True)
    locale = db.Column(db.String(10), unique=False, nullable=False, default="og")
    content = db.Column(db.Text, nullable=False)  # This is effectively read-only
    timestamp = db.Column(db.Integer, default=current_timestamp)
    deleted = db.Column(db.Boolean, default=False)

    @classmethod
    def get_template(cls, fid: str, locale: str, revisions_back: int = 0, include_deleted: bool = False) -> Optional['DbTemplate']:
        qry = cls.query.\
            filter_by(fid=fid).\
            filter_by(locale=locale).\
            filter_by(deleted=include_deleted).\
            order_by(cls.id.desc())

        res = qry.limit(revisions_back+1).all()
        return res[revisions_back] if revisions_back < len(res) else None

    @classmethod
    def add_template(cls, fid: str, locale: str, template: 'TNSTemplatePwndoc') -> Tuple['DbTemplate', bool]:
        if fid == "":
            FlashLog.warning(f"At least one of the templates doesn't have a FID. You can find it on: Status page for problems with templates.")

        previous_template = cls.get_template(fid, locale)
        new_template = DbTemplate(fid=fid, locale=locale)
        new_template._encode_content(template)

        if previous_template:
            if new_template.content == previous_template.content:
                return previous_template, False  # not new

        db.session.add(new_template)
        db.session.commit()
        return new_template, True  # new

    @classmethod
    def delete_template(cls, fid: str, locale: Optional[str] = None):
        qry = sqlalchemy.update(cls).where(cls.fid == fid)
        if locale:
            qry = qry.where(cls.locale == locale)
        qry = qry.values(deleted=True)
        db.session.execute(qry)

        if len(cls.get_current_templates_for_fid(fid)) == 0:
            mapping_res = DbPwndocMapping.query.filter_by(fid=fid).first()
            if mapping_res:
                db.session.delete(mapping_res)

        db.session.commit()

    def get_previous_revision(self) -> Optional['DbTemplate']:
        return self.get_template(self.fid, self.locale, revisions_back=1)

    def decode_content(self) -> Optional['TNSTemplatePwndoc']:
        return TNSTemplatePwndoc.from_json(self.content)  # todo: handle gracefully if the content is invalid or missing?

    def _encode_content(self, template: 'TNSTemplatePwndoc'):
        # note: there is no commit here, this is only a helper function so that encoding and decoding are right next to each other
        self.content = template.to_json(sort_keys=True)

    @classmethod
    def _get_current_templates(cls, fid: Optional[str] = None) -> List['DbTemplate']:
        qry_newest_template_ids = db.session.\
            query(sqlalchemy.func.max(cls.id).label('id')).\
            filter_by(deleted=False)

        if fid:
            qry_newest_template_ids = qry_newest_template_ids.filter_by(fid=fid)

        qry_newest_template_ids = qry_newest_template_ids.\
            group_by(cls.fid, cls.locale). \
            subquery()

        # Warning: never merge it to one query. In one query the order_by before group_by doesn't work as expected.
        res = cls.query.filter(cls.id.in_(qry_newest_template_ids)).all()
        return res

    @classmethod
    def get_all_current_templates(cls, fid: str = None) -> List['DbTemplate']:
        return cls._get_current_templates(fid)

    @classmethod
    def get_history(cls, fid: str, locale: str) -> List['DbTemplate']:
        qry = cls.query.\
            filter_by(fid=fid).\
            filter_by(locale=locale).\
            filter_by(deleted=False).\
            order_by(cls.id.asc())

        res = qry.all()
        return res

    @classmethod
    def get_current_templates_for_fid(cls, fid: str) -> List['DbTemplate']:
        return cls._get_current_templates(fid)

    @classmethod
    def get_all_history(cls) -> List['DbTemplate']:
        qry = cls.query.\
            filter_by(deleted=False).\
            order_by(cls.id.asc())

        res = qry.all()
        return res

    @classmethod
    def get(cls, history_id: int) -> Optional['DbTemplate']:
        res = cls.query.\
            filter_by(id=history_id).\
            first()

        return res
