import hashlib
import secrets
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Any

from .....utils import DictUtils, Crypto, get_object_model_class, init_class_kwargs
from ....base.firebase_collection import FirebaseCollectionBase, getTimestamp
from .device_code_fields import DeviceCodeFields, DeviceCodeFieldsProps, STANDARD_FIELDS, DEVICE_CODE_COLLECTION
from .....utils.response import ApiResponse, Error, ResponseStatus, StatusCode


def _hash(value: str) -> str:
    return hashlib.sha256(value.encode()).hexdigest()


def _generate_user_code() -> str:
    part1 = secrets.token_hex(2).upper()
    part2 = secrets.token_hex(2).upper()
    return f"{part1}-{part2}"


@dataclass(init=False)
class DeviceCode(FirebaseCollectionBase):
    id: str
    device_code: str
    user_code: str
    client_id: str
    scope: str
    user_uid: str
    status: str
    interval: int
    expires_at: str
    last_polled_at: str
    created_at: str

    def __init__(self, dc=None, **kwargs):
        if type(dc) is str:
            dc = {"id": dc}
        (cls_object, keys, data_args) = init_class_kwargs(self, dc, STANDARD_FIELDS, DeviceCodeFieldsProps, DEVICE_CODE_COLLECTION, ['id'], **kwargs)
        super().__init__(**data_args)
        for key in keys:
            setattr(self, key, cls_object[key])

    @staticmethod
    def generate_model(_key_="default_value"):
        model = {}
        props = DeviceCodeFieldsProps
        for k in DictUtils.get_keys(props):
            model[k] = props[k][_key_]
        return model

    @staticmethod
    def from_dict(dc: Any = None, db=None, collection_name=None) -> 'DeviceCode':
        cls_object, keys = get_object_model_class(dc, DeviceCode, DeviceCodeFieldsProps)
        return DeviceCode(cls_object, db=db, collection_name=collection_name)

    def query(self, query_params: list, first=False, limit=None):
        try:
            result = []
            query_result = self.custom_query(query_params, first=first, limit=limit)
            if bool(query_result):
                if first:
                    return DeviceCode.from_dict(dc=query_result, db=self.db, collection_name=self.collection_name)
                else:
                    for doc in query_result:
                        result.append(DeviceCode.from_dict(dc=doc, db=self.db, collection_name=self.collection_name))
                    return result
            return None if first else []
        except Exception as e:
            print('Exception while querying DeviceCode', e)
            return None if first else []

    def to_dict(self):
        return {
            "id": self.id,
            "device_code": self.device_code,
            "user_code": self.user_code,
            "client_id": self.client_id,
            "scope": self.scope,
            "user_uid": self.user_uid,
            "status": self.status,
            "interval": self.interval,
            "expires_at": self.expires_at,
            "last_polled_at": self.last_polled_at,
            "created_at": self.created_at,
        }

    def to_json(self, _fields=None):
        return self.to_dict()

    def get_by_user_code(self, code: str) -> 'DeviceCode':
        return self.query([{"key": DeviceCodeFields.user_code, "comp": "==", "value": code}], first=True)

    def get_by_device_code(self, raw_device_code: str) -> 'DeviceCode':
        hashed = _hash(raw_device_code)
        return self.query([{"key": DeviceCodeFields.device_code, "comp": "==", "value": hashed}], first=True)

    def is_expired(self) -> bool:
        try:
            expires = datetime.fromisoformat(self.expires_at)
            return expires < datetime.now(timezone.utc)
        except Exception:
            return True

    def authorize(self, user_uid: str):
        self.document_reference(self.id).set({
            "user_uid": user_uid,
            "status": "authorized"
        }, merge=True)

    def poll(self) -> dict:
        now = datetime.now(timezone.utc)
        if self.last_polled_at:
            try:
                last = datetime.fromisoformat(self.last_polled_at)
                elapsed = (now - last).total_seconds()
                if elapsed < self.interval:
                    return {"error": "slow_down", "interval": self.interval}
            except Exception:
                pass
        self.document_reference(self.id).set({"last_polled_at": now.isoformat()}, merge=True)
        if self.is_expired():
            return {"error": "expired_token"}
        if self.status == "pending":
            return {"error": "authorization_pending"}
        if self.status == "denied":
            return {"error": "access_denied"}
        return {"status": self.status, "user_uid": self.user_uid}

    @classmethod
    def create(cls, client_id: str, scope: str = "", expires_in: int = 1800) -> tuple:
        raw_device_code = secrets.token_hex(32)
        hashed_device_code = _hash(raw_device_code)
        user_code = _generate_user_code()
        now = datetime.now(timezone.utc)
        expires_at = (now + timedelta(seconds=expires_in)).isoformat()
        model = {
            "device_code": hashed_device_code,
            "user_code": user_code,
            "client_id": client_id,
            "scope": scope,
            "user_uid": "",
            "status": "pending",
            "interval": 5,
            "expires_at": expires_at,
            "last_polled_at": "",
            "created_at": now.isoformat(),
        }
        obj = cls.from_dict(model)
        obj.id = Crypto().generate_id(client_id + "~device~" + user_code)
        doc_ref = DeviceCode(obj.to_json()).document_reference()
        doc_ref.set({**obj.to_json(), "createdAt": getTimestamp()}, merge=True)
        return obj, raw_device_code
