from datetime import datetime, timezone, timedelta
from uuid import uuid4

import jwt
from flask import current_app
from sqlalchemy.ext.hybrid import hybrid_property

from hash_chain.app.extensions import db
from hash_chain.app.extensions.flask_bcrypt import bcrypt
from hash_chain.app.modules.auth.models import BlacklistedToken
from hash_chain.app.util.datetime_util import make_tzaware, localized_dt_string, get_local_utcoffset, utc_now
from hash_chain.app.util.result import Result


class User(db.Model):
    """ User Model for storing user related details """
    __tablename__ = "users"

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    email = db.Column(db.String(255), unique=True, nullable=False)
    password_hash = db.Column(db.String(100), nullable=False)
    registered_on = db.Column(db.DateTime, nullable=False, default=utc_now)
    admin = db.Column(db.Boolean, nullable=False, default=False)
    username = db.Column(db.String(50), unique=True)
    public_id = db.Column(db.String(36), unique=True, default=lambda: str(uuid4()))

    def __repr__(self):
        return (
            f"<User email={self.email}, public_id={self.public_id}, admin={self.admin}>"
        )

    @hybrid_property
    def registered_on_str(self):
        registered_on_utc = make_tzaware(
            self.registered_on, use_tz=timezone.utc, localize=False
        )
        return localized_dt_string(registered_on_utc, use_tz=get_local_utcoffset())

    @property
    def password(self):
        raise AttributeError("password: write-only field")

    @password.setter
    def password(self, password):
        log_rounds = current_app.config.get("BCRYPT_LOG_ROUNDS")
        hash_bytes = bcrypt.generate_password_hash(password, log_rounds)
        self.password_hash = hash_bytes.decode("utf-8")

    def check_password(self, password):
        return bcrypt.check_password_hash(self.password_hash, password)

    def encode_access_token(self):
        """
        Generates the Auth Token
        :return: string
        """
        try:
            now = datetime.now(timezone.utc)
            token_age_h = current_app.config.get("TOKEN_EXPIRE_HOURS")
            token_age_m = current_app.config.get("TOKEN_EXPIRE_MINUTES")
            expire = now + timedelta(hours=token_age_h, minutes=token_age_m)
            if current_app.config["TESTING"]:
                expire = now + timedelta(seconds=5)
            payload = dict(exp=expire, iat=now, sub=self.public_id, admin=self.admin)
            key = current_app.config.get("SECRET_KEY")
            return jwt.encode(payload, key, algorithm="HS256")
        except Exception as e:
            return e

    @classmethod
    def find_by_email(cls, email):
        return cls.query.filter_by(email=email).first()

    @classmethod
    def find_by_public_id(cls, public_id):
        return cls.query.filter_by(public_id=public_id).first()

    @staticmethod
    def decode_access_token(access_token):
        """
        Decodes the auth token
        :param access_token:
        :return: integer|string
        """
        # try:
        if isinstance(access_token, bytes):
            access_token = access_token.decode("ascii")
        if access_token.startswith("Bearer "):
            split = access_token.split("Bearer")
            access_token = split[1].strip()
        try:
            key = current_app.config.get("SECRET_KEY")
            payload = jwt.decode(access_token, key, algorithms=["HS256"])
        except jwt.ExpiredSignatureError:
            error = "Access token expired. Please log in again."
            return Result.Fail(error)
        except jwt.InvalidTokenError:
            error = "Invalid token. Please log in again."
            return Result.Fail(error)

        if BlacklistedToken.check_blacklist(access_token):
            error = "Token blacklisted. Please log in again."
            return Result.Fail(error)

        user_dict = dict(
            public_id=payload["sub"],
            admin=payload["admin"],
            token=access_token,
            expires_at=payload["exp"],
        )
        return Result.Ok(user_dict)
