import hashlib
import json
import uuid
from dataclasses import dataclass
from datetime import datetime
from typing import Any

import pydash
from adafri.cache.lib import CacheClass
from adafri.v1.files import (
    IMAGES_EXTENSIONS,
    File,
    crop_image,
    crop_image_from_url,
    estimate_cropped_file_size,
    estimate_pre_crop_file_size,
    get_file_size_from_url,
    upload_image_from_url,
)
from adafri.v1.gads.data import NATIVE_ADS_REQUIREMENTS as IMAGES_RATIO
from adafri.v1.gads.models.client_customer_id import ClientCustomerId
from firebase_admin.firestore import firestore

from app.test import cache

from ......utils import (
    ArrayUtils,
    BaseClass,
    DateUtils,
    DictUtils,
    get_object_model_class,
    get_request_fields,
    init_class_kwargs,
    integer,
    string,
)
from ......utils.response import ApiResponse, Error, ResponseStatus, StatusCode
from ......v1 import version
from .....account import Account
from .....base.firebase_collection import FirebaseCollectionBase
from ....models.client_customer_id import ClientCustomerId
from .asset_fields import (
    ASSET_CROP_FIELDS,
    ASSETS_COLLECTION,
    ASSETS_PICKABLE_FIELDS,
    FULL_SIZE_INFO_FIELDS,
    STANDARD_FIELDS,
    AssetCropFields,
    AssetCropFieldsProps,
    AssetFullSizeInfoFields,
    AssetFullSizeInfoFieldsProps,
    AssetsFields,
    AssetsFieldsProps,
)

RESPONSIVE_DISPLAY_ADS_UPLOAD_COLLECTION = "responsive_display_ads_upload"


def filter_asset_request_fields(fields, default_fields=ASSETS_PICKABLE_FIELDS):
    request_fields = get_request_fields(fields, default_fields, [default_fields[0]])
    if request_fields is None or bool(request_fields) is False:
        request_fields = [default_fields[0]]
    return request_fields


@dataclass(init=False)
class AssetFullSizeInfo(BaseClass):
    imageUrl: str | None = None
    imageWidth: int | None = None
    imageHeight: int | None = None

    def __init__(self, fullSizeInfo=None, **kwargs):
        (cls_object, keys, data_args) = init_class_kwargs(
            self,
            fullSizeInfo,
            FULL_SIZE_INFO_FIELDS,
            AssetFullSizeInfoFieldsProps,
            None,
            [],
            **kwargs,
        )
        super().__init__(**data_args)
        for key in keys:
            if key == AssetFullSizeInfoFields.imageUrl:
                setattr(self, key, string(cls_object[key], None, True))
            elif key == AssetFullSizeInfoFields.imageWidth:
                setattr(self, key, integer(cls_object[key]))
            elif key == AssetFullSizeInfoFields.imageHeight:
                setattr(self, key, integer(cls_object[key]))
            else:
                setattr(self, key, cls_object[key])

    @staticmethod
    def from_dict(asset: Any) -> "AssetFullSizeInfo":
        cls_object, keys = get_object_model_class(
            asset, AssetFullSizeInfo, AssetsFieldsProps
        )
        _asset = AssetFullSizeInfo(cls_object)
        return _asset

    def to_dict(self, _fields=None) -> "dict | None":
        return DictUtils.remove_none_values(
            {
                "imageUrl": self.imageUrl,
                "imageWidth": self.imageWidth,
                "imageHeight": self.imageHeight,
            }
        )

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


class AssetCrop(BaseClass):
    x: int = None
    y: int = None
    xPosition: int = None
    yPosition: int = None

    def __init__(self, crop=None, **kwargs):
        (cls_object, keys, data_args) = init_class_kwargs(
            self, crop, ASSET_CROP_FIELDS, AssetCropFieldsProps, None, [], **kwargs
        )
        super().__init__(**data_args)
        for key in keys:
            if key == AssetCropFields.x:
                setattr(self, key, integer(cls_object[key], None, True))
            elif key == AssetCropFields.y:
                setattr(self, key, integer(cls_object[key]))
            elif key == AssetCropFields.xPosition:
                setattr(self, key, integer(cls_object[key]))
            elif key == AssetCropFields.yPosition:
                setattr(self, key, integer(cls_object[key]))
            else:
                setattr(self, key, cls_object[key])

    @staticmethod
    def from_dict(asset: Any) -> "AssetCrop":
        cls_object, keys = get_object_model_class(
            asset, AssetCrop, AssetCropFieldsProps
        )
        _asset = AssetCrop(cls_object)
        return _asset

    def to_dict(self, _fields=None):
        return DictUtils.remove_none_values(
            {
                "x": self.x,
                "y": self.y,
                "xPosition": self.xPosition,
                "yPosition": self.yPosition,
            }
        )


@dataclass(init=False)
class Asset(FirebaseCollectionBase):
    id: str = None
    hash: str = None
    name: str = None
    accountId: str = None
    owner: str = None
    clientCustomerId: str = None
    assetId: int = None
    imageMimeType: str = None
    imageFileSize: int = None
    assetName: str = None
    assetSubType: str = None
    fullSizeInfo: AssetFullSizeInfo = None
    usage: list[str] = None
    useFor: str = None
    width: int = None
    height: int = None
    imageUrl: str = None
    url: str = None
    originalImageInfo: str = None
    crop: AssetCrop = None
    crop_state: str = None
    content_type: str = None
    data: str = None
    size: int = None
    file_url: str = None
    type: str = None
    state: str = None
    account: Account | None = None
    customer: ClientCustomerId | None = None
    adgroup: Any | None = None
    __baseFields = AssetsFieldsProps

    def __init__(self, asset=None, **kwargs):
        _asset = asset
        if type(asset) is str:
            _asset = {"id": asset}
        if type(asset) is dict:
            _asset = _asset.copy()
            if "aacid" in _asset and "accountId" not in _asset:
                _asset["accountId"] = _asset["aacid"]
            if "url" in _asset and "imageUrl" not in _asset:
                _asset["imageUrl"] = _asset["url"]
            if "originalImageInfo" in _asset and type(asset) is dict:
                info = _asset.copy()["originalImageInfo"]
                if isinstance(info, dict) is True:
                    _asset["originalImageInfo"] = json.dumps(info)
                else:
                    _asset["originalImageInfo"] = info

        (cls_object, keys, data_args) = init_class_kwargs(
            self,
            _asset,
            STANDARD_FIELDS,
            AssetsFieldsProps,
            ASSETS_COLLECTION,
            ["id"],
            **kwargs,
        )
        super().__init__(**data_args)
        for key in keys:
            if key == AssetsFields.assetId:
                setattr(self, key, integer(cls_object[key]))
            elif key == AssetsFields.size:
                setattr(self, key, integer(cls_object[key]))
            elif key == AssetsFields.imageFileSize:
                setattr(self, key, integer(cls_object[key]))
            elif key == AssetsFields.width:
                setattr(self, key, integer(cls_object[key]))
            elif key == AssetsFields.height:
                setattr(self, key, integer(cls_object[key]))
            elif key == AssetsFields.fullSizeInfo:
                if bool(cls_object[key]) is True:
                    setattr(self, key, AssetFullSizeInfo.from_dict(cls_object[key]))
                else:
                    setattr(self, key, None)
            elif key == AssetsFields.imageMimeType:
                setattr(self, key, string(cls_object[key], None, True))
            elif key == AssetsFields.usage:
                setattr(self, key, ArrayUtils.to_array(cls_object[key]))
            elif key == AssetsFields.useFor:
                setattr(self, key, string(cls_object[key], None, True))
            elif key == AssetsFields.file_url:
                setattr(self, key, string(cls_object[key], None, True))
            elif key == AssetsFields.type:
                setattr(self, key, string(cls_object[key], None, True))
            elif key == AssetsFields.imageUrl:
                setattr(self, key, string(cls_object[key], None, True))
            elif key == AssetsFields.originalImageInfo:
                setattr(self, key, string(cls_object[key], None, True))
            elif key == AssetsFields.clientCustomerId:
                setattr(self, key, string(cls_object[key]))
            elif key == AssetsFields.crop_state:
                setattr(self, key, string(cls_object[key], None, True))
            elif key == AssetsFields.content_type:
                setattr(self, key, string(cls_object[key], None, True))
            elif key == AssetsFields.state:
                setattr(self, key, string(cls_object[key], None, True))
            elif key == AssetsFields.hash:
                setattr(self, key, string(cls_object[key], None, True))
            elif key == AssetsFields.account:
                setattr(self, key, Account.from_dict(cls_object[key]))
            elif key == AssetsFields.customer:
                setattr(self, key, ClientCustomerId.from_dict(cls_object[key]))
            elif key == AssetsFields.adgroup:
                setattr(self, key, cls_object[key])
            elif key == AssetsFields.crop and bool(cls_object[key]) is True:
                setattr(self, key, AssetCrop.from_dict(cls_object[key]))
            else:
                setattr(self, key, cls_object[key])
        self.__normalize__()
        self.cache = CacheClass("asset", Asset)


    def __normalize__(self):
        if bool(self.fullSizeInfo) is True and bool(self.fullSizeInfo.imageUrl) is True:
            if bool(self.imageUrl) is False:
                self.imageUrl = self.fullSizeInfo.imageUrl
                self.width = self.fullSizeInfo.imageWidth
                self.height = self.fullSizeInfo.imageHeight
        if bool(self.url) is True and bool(self.imageUrl) is False:
            self.imageUrl = self.url
        if bool(self.imageUrl) is True and bool(self.url) is False:
            self.url = self.imageUrl
        if bool(self.file_url) is False and bool(self.imageUrl) is True:
            self.file_url = self.imageUrl
    @staticmethod
    def generate_model(_key_="default_value"):
        user = {}
        props = AssetsFieldsProps
        for k in DictUtils.get_keys(props):
            user[k] = props[k][_key_]
        return user

    @staticmethod
    def from_dict(asset: Any, db=None, collection_name=None) -> "Asset":
        # print('asset', asset)
        _asset = None
        if type(asset) is dict:
            _asset = asset.copy()
            if "aacid" in _asset and "accountId" not in _asset:
                _asset["accountId"] = _asset["aacid"]
            if "url" in _asset and "imageUrl" not in _asset:
                _asset["imageUrl"] = _asset["url"]
            if "originalImageInfo" in _asset and type(asset) is dict:
                info = _asset.copy()["originalImageInfo"]
                if isinstance(info, dict) is True:
                    # print('info dict')
                    _asset["originalImageInfo"] = json.dumps(info)
                else:
                    _asset["originalImageInfo"] = info
        else:
            _asset = asset

        cls_object, keys = get_object_model_class(_asset, Asset, AssetsFieldsProps)
        _asset = Asset(cls_object, db=db, collection_name=collection_name)
        if None in _asset.usage:
            print('asset none', cls_object)
        return _asset
    def getAssetNative(self, id: str):
        db = FirebaseCollectionBase(collection_name='assets-native')
        doc = db.document_reference(id).get()
        if doc.exists is True:
            return doc.to_dict()
        return None
    def check_parsable(self, element: str):
        obj = None;
        try:
            obj = json.loads(element);
            keys = ['x', 'y', 'xPosition', 'yPosition']
            for key in keys:
                if key not in obj:
                    return {"is_url": False, "parsed": None}
            return {"is_url": False, "parsed": obj}
        except Exception:
            # print('ex', e)
            return {"is_url": True, "parsed": None, "url": element}
    def crop_image(self, element, folder: str):
        crop = None;
        if 'crop' in element:
            crop = element['crop'];
            crop['yPosition'] = crop['y'] + crop['yPosition'];
            crop['xPosition'] = crop['x'] + crop['xPosition'];
        else:
            crop = {
                'x': element['x'],
                'y': element['y'],
                'xPosition': element['xPosition'],
                'yPosition': element['yPosition']
            }
        data_id = None
        data_url = None
        if 'id' in element:
            data_id = element['id'];
        if 'originalImage' in element:
            data_url = element['originalImage'];
        elif 'originalImageInfo' in element:
            data_url = element['originalImageInfo']['url'];
        else:
            # data['url'] = element['url'];
            data_url = element['url'];
        cropping = crop_image_from_url(data_url, self.accountId, crop, folder, 'adafri_upload', data_id);
        if cropping is None:
            return None
        return cropping
    def createAssetFile(self):
        file = None
        if bool(self.hash) is False:
            if bool(self.url) is True:
                try:
                    file = upload_image_from_url(file=self.url, upload_path='', extensions=IMAGES_EXTENSIONS.copy(),accountId=self.accountId, upload_folder=RESPONSIVE_DISPLAY_ADS_UPLOAD_COLLECTION, process=False)
                    if file is not None:
                        return ApiResponse(ResponseStatus.OK, StatusCode.status_200, file, None)
                except:
                    pass
            return ApiResponse(ResponseStatus.ERROR, StatusCode.status_400, None, Error(f"Cannot build asset file ref for {self.id}", "INVALID_REQUEST", 1))
        file = File({"id": self.hash}, collection_name=RESPONSIVE_DISPLAY_ADS_UPLOAD_COLLECTION).get()
        if file is not None:
            return ApiResponse(ResponseStatus.OK, StatusCode.status_200, file, None)
        return ApiResponse(ResponseStatus.OK, StatusCode.status_204, None, None)

         
    def build_parsable(self, file=None):
        _file = file if file is not None else File(file={"id": self.hash}, collection_name=RESPONSIVE_DISPLAY_ADS_UPLOAD_COLLECTION).get()
        parsable = self.check_parsable(self.url)
        if parsable is not None:
            if parsable['is_url'] is True and parsable['parsed'] is None:
                if _file is None:
                    # return ApiResponse(ResponseStatus.ERROR, StatusCode.status_500, None, Error(f"Need to create file {self.imageUrl} {self.hash}", "INVALID_REQUEST", 1))
                    asset_create = self.createAssetFile()
                    if asset_create.status == ResponseStatus.OK:
                        if asset_create.status_code == StatusCode.status_200:
                            return asset_create;
                    return ApiResponse(ResponseStatus.ERROR, StatusCode.status_500, None, Error(f"Cannot find or create asset file to use {self.id}", "INVALID_REQUEST", 1))
                return ApiResponse(ResponseStatus.OK, StatusCode.status_200, _file, None)
            
            # if parsable['is_url'] is False and parsable['parsed'] is not None:
            #     crop = self.crop_image(parsable['parsed'], 'assets-native')
            #     if crop is not None:
            #         self.file_url = crop.file_url
            #         self.width = crop.width
            #         self.height = crop.height
            #         eligibility = self.check_eligibility(file)
            #         if eligibility.status == ResponseStatus.ERROR:
            #             return eligibility
            #         self.crop_state = "done"
            #         return ApiResponse(ResponseStatus.OK, StatusCode.status_200, Asset.from_dict(self.to_dict()), None)  
        return ApiResponse(ResponseStatus.ERROR, StatusCode.status_400, None, Error(f"Cannot build asset from parsed elements {self.id}", "INVALID_REQUEST", 1))           
    
    
    def build_asset(self, file: File | None=None, cropAsset = True, fetch = True, createAsset = True, test = False):
        asset_data = self;
        if fetch is True and bool(self.id) is True:
            asset_data = self.get()
        if asset_data is None:
            return ApiResponse(ResponseStatus.ERROR, StatusCode.status_400, None, Error(f"Cannot find asset {self.id}", "INVALID_REQUEST", 1))

        if self.state is not None and asset_data.state!=self.state:
            asset_data.state = self.state
        if self.adgroup is not None and asset_data.adgroup is None:
            asset_data.adgroup = self.adgroup
        if self.account is not None and asset_data.account is None:
            asset_data.account = self.account
        if self.customer is not None and asset_data.customer is None:
            asset_data.customer = self.customer
        copy = None
        if asset_data.adgroup is not None and asset_data.adgroup.clientCustomerId!=asset_data.clientCustomerId:
            # print("need to copy and create asset", asset_data.id)
            asset_copy = asset_data.to_dict().copy()
            copy = asset_data.id
            if "id" in asset_copy:
                del asset_copy["id"]
            if "assetId" in asset_copy:
                del asset_copy["assetId"]
            asset_copy["name"] = f"Asset copy {uuid.uuid1()}"
            new_asset = Asset.from_dict(asset_copy)
            new_asset.adgroup = asset_data.adgroup
            new_asset.clientCustomerId = asset_data.adgroup.clientCustomerId
            asset_data = new_asset
            # return ApiResponse(ResponseStatus.ERROR, StatusCode.status_500, None, Error(f"need to create new asset {json.dumps(new_asset.to_dict())}", "INVALID_REQUEST", 1))
        if asset_data.check_eligibility(file).status == ResponseStatus.OK and copy is None:
            return ApiResponse(ResponseStatus.OK, StatusCode.status_200, Asset.from_dict(asset_data.to_dict()), None)
        parsable = asset_data.build_parsable(file)
        if parsable.status == ResponseStatus.ERROR:
            return parsable
        _file: File = parsable.data
        # print('parsable created file', _file.id, self.id)
        asset_data.hash = _file.id
        setup = asset_data.setup_asset(_file)
        if setup.status == ResponseStatus.ERROR:
            return setup
        id = asset_data.id
        # if copy is not None:
        #     print("copyed", copy, id)
        build = None
        if bool(id) is False:
            id = asset_data.setup_asset_id(file)
            # print("new id", id)
            # print("new imageUrl", asset_data.imageUrl)
            # print("new url", asset_data.url)
            data_asset = Asset({"id": id}).get()
            # print("data_asset", data_asset)
            if data_asset is not None:
                if data_asset.check_eligibility(file).status == ResponseStatus.OK:
                    return ApiResponse(ResponseStatus.OK, StatusCode.status_302, data_asset, None)
                asset_data = data_asset

        build = asset_data.handle_asset(file=_file, test = test, cropAsset=cropAsset);
        if build.status == ResponseStatus.OK:
            asset: Asset = build.data;
            # print("asset passed build successfully", asset.id)
            if bool(asset.id) is False:
                asset.id = id
                if createAsset is True:
                    # print('creating not created asset done', asset.id)
                    create = self.create(asset.to_dict(), asset.accountId, _file, asset.clientCustomerId, False, test)
                    # print('creating not created asset done', create.data.id if create.data is not None else create.data)
                    return create
        return build
        # return ApiResponse(ResponseStatus.ERROR, StatusCode.status_500, None, Error(f"Cannot build asset {self.id}", "INVALID_REQUEST", 1))

    def handle_asset(self, file: File | None=None, cropAsset=True, test = False):
        asset = None
        eligibility = self.check_eligibility(file)
        if eligibility.status_code == StatusCode.status_200:
            if self.need_crop() is True:
                # print('need crop', self.id)
                # estimated_file_size = estimate_cropped_file_size(file.width, file.height, file.size, self.crop.x, self.crop.y, self.crop.xPosition, self.crop.yPosition)
                # print('estimated_file_size', estimated_file_size)
                self.crop.xPosition = self.crop.x + self.crop.xPosition
                self.crop.yPosition = self.crop.y + self.crop.yPosition
                if cropAsset is True:
                    crop = crop_image(
                        self.url, self.accountId, self.crop.to_dict(), self.collection_name
                    )
                    # print('crop', crop)
                    if isinstance(crop, dict) is True and bool(crop) is True:
                        self.size = crop['size']
                        new_asset = {**self.to_dict().copy(), **crop}
                        new_asset['imageUrl'] = dict(crop).get('file_url')
                        new_asset["crop_state"] = "done"
                        if bool(self.id):
                            new_asset["id"] = self.id
                        if test is True:
                            asset = Asset.from_dict(new_asset.copy())
                        if bool(self.id) is True:
                            update = self.update(new_asset, True)
                            if update.status == ResponseStatus.OK:
                                asset = Asset.from_dict(new_asset.copy())
                        else:
                            print('asset not created yet we need to skip the update')
                            asset = Asset.from_dict(new_asset.copy())

                else:
                    # print("skipping asset crop")
                    asset = Asset.from_dict(self.to_dict())
                # asset = Asset.from_dict(self.to_dict())
            else:
                # print('asset no need crop')
                if file is not None:
                    file_data = DictUtils.merge_dict(DictUtils.omit_fields(file.to_dict(), ['id', 'createdAt', 'createdBy']), self.to_dict(), True)
                    asset = Asset.from_dict(file_data)
                    asset.imageUrl = file.file_url
                    asset.size = file.size
                    asset.hash = file.id
                    asset.url = file.url
                    asset.content_type = file.content_type
                else:
                    asset = Asset.from_dict(self.to_dict())
                asset.usage = self.usage
                asset.useFor = self.useFor
                asset.owner = self.owner
                asset.accountId = self.accountId
                asset.crop_state = "done"
            return ApiResponse(ResponseStatus.OK, StatusCode.status_200, asset, None)
        else:
            return eligibility
            

    def getHash(self):
        if bool(self.hash) is True:
            file = File({"id", self.hash}).get()
            return File
        return None

    @staticmethod
    def find_customer(id: str):
        if id is None or bool(id) is False:
            return ApiResponse(
                ResponseStatus.ERROR,
                StatusCode.status_400,
                None,
                Error("Customer not found", "INVALID_REQUEST", 1),
            )

        client_customer_id = ClientCustomerId(id).get()
        if client_customer_id is None:
            return ApiResponse(
                ResponseStatus.ERROR,
                StatusCode.status_400,
                None,
                Error("Customer not found", "INVALID_REQUEST", 1),
            )
        return ApiResponse(
            ResponseStatus.OK, StatusCode.status_200, client_customer_id, None
        )
    def getAssets(self, aacid, fields=None):
        assets = []
        assets_ = self.query(Asset, "asset", query_params=[{"key": "accountId", "comp": "==", "value": aacid}])
        request_fields = filter_asset_request_fields(fields)
        # print('account query', account)
        for asset in assets_:
            assets.append(asset)
            # campaigns.append(D(campaign.to_dict(request_fields)))

        return assets

    @staticmethod
    def findAsset(id, aacid=None):
        isID = False
        try:
            id = int(id)
        except Exception as e:
            isID = True
        if isID is True:
            asset = Asset.from_dict({"id": id}).get(ASSETS_PICKABLE_FIELDS)
            if asset is None:
                return None
            return asset
        if (
            id is None
            or aacid is None
            or (isinstance(id, str) and len(id) == 0)
            or (isinstance(aacid, str) and len(aacid)) == 0
            or (isinstance(id, int) and id == 0)
        ):
            return None
        query = [
                {"key": "accountId", "comp": "==", "value": aacid},
                {"key": "assetId", "comp": "==", "value": id},
            ]
        query_key = hashlib.sha1(str(json.dumps(query)).encode()).hexdigest()
        asset_cache = cache.get(query_key)
        if asset_cache is not None:
            return Asset.from_dict(asset_cache)
        asset = Asset(None).query(
            Asset,
            "asset",
            query_params=query,
            first=True,
        )
        cache.set(query_key, asset)
        return asset

    def validate_customer(self):
        validate = Asset.find_customer(self.clientCustomerId)
        return validate

    def to_dict(self):
        fullSize = None
        if self.fullSizeInfo is not None:
            fullSize = self.fullSizeInfo.to_dict()
        crop = None
        if bool(self.crop) is True:
            crop = self.crop.to_dict()
        return DictUtils.remove_none_values(
            {
                "id": self.id,
                "name": self.name,
                "accountId": self.accountId,
                "owner": self.owner,
                "clientCustomerId": self.clientCustomerId,
                "assetId": self.assetId,
                "imageMimeType": self.imageMimeType,
                "imageFileSize": self.imageFileSize,
                "assetName": self.assetName,
                "assetSubType": self.assetSubType,
                "usage": self.usage,
                "useFor": self.useFor,
                "width": self.width,
                "height": self.height,
                "imageUrl": self.imageUrl,
                "file_url": self.file_url,
                "type": self.type,
                "size": self.size,
                "url": self.url,
                "originalImageInfo": self.originalImageInfo,
                "fullSizeInfo": fullSize,
                "crop": crop,
                "data": self.data,
                "hash": self.hash,
                "state": self.state,
                "content_type": self.content_type,
                "customer": self.customer.to_dict() if self.customer is not None else None,
                "account": self.account.to_dict() if self.account is not None else None,
                "adgroup": self.adgroup
            }
        )

    def get(self, fields=None):
        id = getattr(self, "id", None)
        assetId = getattr(self, "assetId", None)
        asset: Asset | None = None
        if bool(id) is False and bool(assetId) is True:
            query_array = [{"key": "assetId", "comp": "==", "value": assetId}]
            query_cache_key = hashlib.sha1(str(json.dumps(query_array)).encode()).hexdigest()
            asset = self.cache.get(query_cache_key)
            if asset is not None:
                asset.correct_crop()
                asset.correct_use_for()
                return asset
            asset = Asset(asset=None).query(
                Asset,
                "asset",
                query_params=[{"key": "assetId", "comp": "==", "value": assetId}],
            )
            asset.correct_crop()
            asset.correct_use_for()
            self.cache.set(query_cache_key, asset)
            return asset
        if bool(id) is None:
            return None
        asset = self.cache.get(self.id)
        if asset is not None:
            return asset
        doc = self.document_reference().get()
        if doc.exists is False:
            return None
        data = {"id": doc.id, **doc.to_dict()}
        asset = Asset.from_dict(
            asset=Asset.from_dict(asset=data).to_dict(),
            db=self.db,
            collection_name=self.collection_name,
        )
        asset.correct_crop()
        asset.correct_use_for()
        self.cache.set(asset.id, asset)
        return asset

    # def crop_image(self):
    def make_asset(self, client_customer_id=None):
        if bool(self.assetId) is True:
            if (
                self.clientCustomerId == client_customer_id
                and bool(client_customer_id) is True
            ):
                return Asset.from_dict(self.to_dict())
        if bool(self.imageUrl) is False:
            if bool(self.crop) is True:
                return None

    def need_crop(self):
        if not self.hasCrop():
            return False
        if self.crop_state is not None and self.crop_state.lower() == "done":
            return False
        return True

    def determine_dimensions(self, crop: AssetCrop):
        if crop is not None and bool(crop.to_dict()) is True:
            width = crop.xPosition
            height = crop.yPosition
            return (width, height)

    def _check_aspect_ratio(self, width: int, height: int, target_ratio: str) -> bool:
        """
        Check if image aspect ratio matches the target ratio.
        
        Args:
            target_ratio: String like "1.91:1", "1:1", "4:3", etc.
        
        Returns:
            bool: True if aspect ratio is within acceptable range
        """
        try:
            if not target_ratio or not hasattr(self, 'width') or not hasattr(self, 'height'):
                return True  # Skip ratio check if not specified
            
            # Parse target ratio
            if ':' in target_ratio:
                target_w, target_h = map(float, target_ratio.split(':'))
                target_ratio_value = target_w / target_h
            else:
                # Handle single number ratios (e.g., "1.91" means 1.91:1)
                target_ratio_value = float(target_ratio)
            
            # Calculate current image ratio
            current_ratio = width / height
            
            # Allow some tolerance (e.g., ±2%)
            tolerance = 0.02
            min_ratio = target_ratio_value * (1 - tolerance)
            max_ratio = target_ratio_value * (1 + tolerance)
            
            ratio_ok = min_ratio <= current_ratio <= max_ratio
            
            # Debug info
            # if not ratio_ok:
            #     print(f"Aspect ratio mismatch: current={current_ratio:.3f}, target={target_ratio_value:.3f}, tolerance=±{tolerance*100}%")
            
            return ratio_ok
            
        except Exception as e:
            print(f"Error checking aspect ratio: {e}")
            return True  # Skip ratio check on error
    
    def correct_crop(self):
        if not self.hasCrop() or None in [self.width, self.height]:
            return;
        x = self.crop.x
        y = self.crop.y
        xPosition = self.crop.xPosition
        yPosition = self.crop.yPosition
        xDiff = xPosition - x
        yDiff = yPosition - y
        if xDiff == self.width:
            xPosition =  xDiff
        if yDiff == self.height:
            yPosition =  yDiff
        self.crop.xPosition = xPosition
        self.crop.yPosition = yPosition
        
    def correct_use_for(self):
        width = self.width
        height = self.height
        if self.hasCrop():
            width = self.crop.xPosition
            height = self.crop.yPosition
        if self._check_aspect_ratio(width, height, "1:1"):
            self.useFor = "sqr"
        elif self._check_aspect_ratio(width, height, "4:1"):
            self.useFor = "rect"
        elif self._check_aspect_ratio(width, height, "1.91:1"):
            self.useFor = "rect"
    
    def compare_size(self, v, file: File | None = None):
        # print('compare', self.id, self.need_crop())
        # if self.id =="03e638a2020758dc9cb3c18286dc5b0a0fe90677":
        #     print('compare', self.need_crop(), self.hasCrop())
        if self.need_crop() is True:
            self.correct_crop()
            crop = self.crop.to_dict().copy()
            if bool(crop) is True:
                if self.crop.xPosition > v['maxWidth'] or self.crop.yPosition > v['maxHeight']:
                    return ApiResponse(ResponseStatus.ERROR, StatusCode.status_400, None, Error(f"File width or height is too large for crop {self.crop.xPosition}x{self.crop.yPosition} over {v['maxWidth']}x{v['maxHeight']} for {self.id if self.id is not None else self.state if self.state is not None else self.assetId if self.assetId is not None else 'unknown'}", "INVALID_REQUEST", 1))
                if self.crop.xPosition < v['minWidth'] or self.crop.yPosition < v['minHeight']:
                    return ApiResponse(ResponseStatus.ERROR, StatusCode.status_400, None, Error(f"File width or height is too small for crop {self.crop.xPosition}x{self.crop.yPosition} over {v['minWidth']}x{v['minHeight']} for {self.id if self.id is not None else self.state if self.state is not None else self.assetId if self.assetId is not None else 'unknown'}", "INVALID_REQUEST", 1))
                ratio_ok = self._check_aspect_ratio(self.crop.xPosition, self.crop.yPosition, v['ratio'])
                if ratio_ok is False:
                    return ApiResponse(ResponseStatus.ERROR, StatusCode.status_400, None, Error(f"Invalid asset ({self.id}) aspect ratio ({v['ratio']}) ({json.dumps(self.crop.to_dict())}) for {self.id if self.id is not None else self.state if self.state is not None else self.assetId if self.assetId is not None else 'unknown'}", "INVALID_REQUEST", 1))
        else:
            check = [bool(self.size), bool(self.width), bool(self.height)]
            width = 0
            height = 0
            size = 0
            if False in check:
                if file is not None:
                    # print('using file', file.file_url)
                    width = file.width
                    height = file.height
                    size = file.size
            else:
                # print('using self')
                width = self.width
                height = self.height
                size = self.size
            if self.hasCrop():
                self.correct_crop()
                width = self.crop.xPosition
                height = self.crop.yPosition
            # print(width, height, size)
            if size is not None and size // 1024 > v['maxFileSize']:
                return ApiResponse(ResponseStatus.ERROR, StatusCode.status_400, None, Error(f"File size is too large {size // 1024}Ko over {v['maxFileSize']}Ko", "INVALID_REQUEST", 1))
            if width < v['minWidth'] or height < v['minHeight']:
                return ApiResponse(ResponseStatus.ERROR, StatusCode.status_400, None, Error(f"File width or height is too small ({width}x{height}) for {self.id if self.id is not None else self.state if self.state is not None else 'asset'} over {v['minWidth']}x{v['minHeight']}", "INVALID_REQUEST", 1))
            if width > v['maxWidth'] or height > v['maxHeight']:
                return ApiResponse(ResponseStatus.ERROR, StatusCode.status_400, None, Error(f"File width or height is too large ({width}x{height}) max ({v['maxWidth']}x{v['maxHeight']}) for {self.id if self.id is not None else self.state if self.state is not None else 'asset'}", "INVALID_REQUEST", 1))
            ratio_ok = self._check_aspect_ratio(width, height, v['ratio'])
            if ratio_ok is False:
                return ApiResponse(ResponseStatus.ERROR, StatusCode.status_400, None, Error(f"Invalid asset ({self.id}) aspect ratio ({v['ratio']}) ({width}x{height}) for {self.id if self.id is not None else self.state if self.state is not None else 'asset'}", "INVALID_REQUEST", 1))
        return ApiResponse(ResponseStatus.OK, StatusCode.status_200, None, None)
    def check_eligibility(self, file: File | None = None):
        # size_before = get_file_size_from_url(file.file_url)
        # print('size_before', size_before)
        is_valid = None
        error = None
        for usage in self.usage:
            # print('usage', usage)
            asset_type = "marketing_image"
            if usage == "image":
                if self.useFor == "sqr":
                    asset_type = "square_marketing_image"
            else:
                asset_type = "logo_landscape"
                if self.useFor == "sqr":
                    asset_type = "logo"
            v = pydash.find(IMAGES_RATIO, lambda x: x["type"] == asset_type)
            # print('type', asset_type)
            is_valid = self.compare_size(v, file)
            if is_valid.status_code != 200:
                error = is_valid
                break
        # file_size = estimate_pre_crop_file_size(file.width, file.height,file.size, self.crop.xPosition, self.crop.yPosition)
        # print('estimated file_size', file_size)
        if error is not None:
            return error        
        return ApiResponse(ResponseStatus.OK, StatusCode.status_200, None, None)

    def checkpoint_asset_account(self, act: str | Account | None = None, state: str | None = None):
        error = {"state": state if state is not None else self.state}
        account = self.account;
        id = self.accountId
        if  bool(id) is False:
            if act is not None:
                if isinstance(act, str):
                    id = act
                else:
                    id = act.id
        if bool(id) and (account is None or bool(account) is False or bool(account.id) is False):
            account = Account(id).get()
        if account is not None:
            self.account = account
            return ApiResponse(ResponseStatus.OK, StatusCode.status_200, account, None)
        error["message"] = f"Cannot found account for asset {self.id}"
        return ApiResponse(ResponseStatus.ERROR, StatusCode.status_400, None, Error(error, "INVALID_REQUEST", 1))
    def checkpoint_asset_customer(self, customer: str | ClientCustomerId | None = None, state: str | None = None):
        error = {"state": self.state if self.state is not None else state}
        clientCustomer = self.customer
        if clientCustomer is not None:
            return ApiResponse(ResponseStatus.OK, StatusCode.status_200, clientCustomer, None)
        id = self.clientCustomerId
        if id is None:
            if customer is not None:
                if isinstance(customer, str):
                    id = customer
                else:
                    id = customer.id
        if clientCustomer is None or bool(clientCustomer) is False or bool(clientCustomer.id) is False:
            if bool(id):
                clientCustomer = ClientCustomerId(id).get()
            else:
                customers = ClientCustomerId().getDefault()
                if bool(customers) is True:
                    clientCustomer = customers[0]
                    self.customer = clientCustomer
                    return ApiResponse(ResponseStatus.OK, StatusCode.status_200, clientCustomer, None)
        if clientCustomer is not None:
            self.customer = clientCustomer
            return ApiResponse(ResponseStatus.OK, StatusCode.status_200, clientCustomer, None)
        error["message"] = f"Cannot found customer {id} for asset {self.id}"
        return ApiResponse(ResponseStatus.ERROR, StatusCode.status_400, None, Error(error, "INVALID_REQUEST", 1))

    def setup_asset_id(self, file: File | None = None):
        id_data = DictUtils.pick_fields(self.to_dict(), ["imageUrl", "url", "accountId", "owner"])
        if bool(self.assetId) is True:
            id_data["assetId"] = self.assetId
        if self.hasCrop():
            id_data["crop"] = self.crop.to_dict()
        else:
            if file is not None:
                id_data["width"] = file.width
                id_data["height"] = file.height
        return hashlib.sha1(str(json.dumps(id_data)).encode()).hexdigest()

    def setup_asset(self, file: File | None):
        account_check = self.checkpoint_asset_account()
        if account_check.status == ResponseStatus.ERROR:
            return account_check
        account: Account = account_check.data
        customer_check = self.checkpoint_asset_customer()
        if customer_check.status == ResponseStatus.ERROR:
            return customer_check
        customer: ClientCustomerId = customer_check.data
        self.accountId = account.id
        self.clientCustomerId = customer.id
        self.owner = account.owner
        self.createdBy = f"users/{account.owner}"
        if self.need_crop():
            dim = self.determine_dimensions(self.crop)
            self.width = dim[0]
            self.height = dim[1]
        else:
            if file is not None:
                self.width = file.width
                self.height = file.height
        return ApiResponse(ResponseStatus.OK, StatusCode.status_200, Asset.from_dict(self.to_dict()), None)


    
    
    def hasCrop(self):
        if bool(self.crop) is False:
            return False
        return self.crop.x is not None and self.crop.y is not None and self.crop.xPosition is not None and self.crop.yPosition is not None

        
    def prepare_asset(self, data, _account: Account | None, file: File | None=None, _customerId=None, testMode=False):
        state = dict(data).get("state", None)
        error = {"state": state}
        data_create = DictUtils.dict_from_keys(
            data, AssetsFields().filtered_keys("mutable", True)
        )
        if bool(data_create) is False:
            error["message"] = "Invalid request required valid mutable fields"
            return ApiResponse(
                ResponseStatus.ERROR,
                StatusCode.status_400,
                None,
                Error(error, "INVALID_REQUEST", 1),
            )
        asset_create = Asset.from_dict(data_create.copy())
        asset_create.state = state
        data_asset = asset_create.build_asset(file, True, False, False, testMode)
        if data_asset.status_code != 200:
            return data_asset
        print('second eligibility', data_asset.data.to_dict())
        validity = data_asset.data.check_eligibility(file)
        if validity.status_code != 200:
            print('validity', validity.error.message)
            error["message"] = validity.error.message
            return ApiResponse(
                ResponseStatus.ERROR,
                StatusCode.status_400,
                None,
                Error(error, "INVALID_ASSET", 1),
            )
        # data_create = data_asset.to_dict()

        # document_id = self.collection().document().id
        # if "assetId" in data and bool(data["assetId"]) is True:
        #     document_id = str(data["assetId"])
        model = Asset.generate_model()
        create = DictUtils.merge_dict(data_asset.data.to_dict(), model, True)
        asset = Asset.from_dict(create)
        if state is not None and bool(state) is True:
            asset.state = str(state)
        # print('asset valid', asset.data)
        return ApiResponse(
            ResponseStatus.OK, StatusCode.status_200, asset, None
        )
    def create(self, data: dict, _account, file: File=None, _customerId=None, prepare = True, testMode=False):
        # creation_date = created_at * 1000
        # return DateUtils.from_timestamp(creation_date).isoformat()
        # try:
        # print('campaign account', data)
        values = None
        state = dict(data).get("state", None)
        error = {"state": state}
        if bool(prepare) is True:
            prepared = self.prepare_asset(data, _account, file, _customerId, testMode)
            if prepared.status_code == StatusCode.status_200:
                values = prepared.data
            else:
                if prepared.status_code == StatusCode.status_302:
                    prepared.status_code = 200
                return prepared
        else:
            values = Asset.from_dict(data)
        document_id = None
        if bool(values.id) is False:
            error["message"] = "Invalid request id not present"
            return ApiResponse(
                ResponseStatus.ERROR,
                StatusCode.status_400,
                None,
                Error(error, "INVALID_REQUEST", 1),
            )
        document_id = str(values.id)

        model = Asset.generate_model()
        data_create = DictUtils.merge_dict(values.to_dict(), model, True)
        create = {**data_create, "createdAt": firestore.SERVER_TIMESTAMP}
        del create["state"]
        if testMode is False:
            doc_ref = self.collection().document(document_id)
            doc_ref.set(create, True)
        # print("create", create)
        asset = Asset.from_dict(create)
        print("create asset successfully done", asset.id)
        if state is not None and bool(state) is True:
            asset.state = str(state)
        asset.correct_use_for()
        self.cache.set(asset.id, asset)
        return ApiResponse(
            ResponseStatus.OK, StatusCode.status_200, asset, None
        )
        # Return the created campaign object.  # Return the created campaign object.  # Return the created campaign object.  # Return the created campaign object.  # Return the created campaign object.  # Return the created campaign object.  # Return the created campaign object.  # Return the created campaign object.  # Return the created campaign object.  # Return the created campaign object.  # Return the created campaign object.  # Return the
        # except Exception as e:
        #     print(e)
        #     return {"error": "Exception creating campaign"};

    def update(self, data, silent=True):
        try:
            data_update = {}
            changed_fields = []
            if silent is True:
                data_update = pydash.pick(
                    data, AssetsFields().filtered_keys("pickable", True)
                )
            else:
                last_value = self.to_dict()
                print("data update", data)
                # print('last', last_value);
                filtered_value = pydash.pick(
                    data, AssetsFields().filtered_keys("editable", True)
                )
                # print('filtered', filtered_value);
                new_value = DictUtils.merge_dict(filtered_value, last_value, True)
                _changed_fields = DictUtils.compare_nested_objects(
                    last_value, new_value
                )
                changed_fields = ArrayUtils.filter(
                    DictUtils.get_keys(_changed_fields),
                    lambda x: str(x).find(".") == -1,
                )
                data_update = DictUtils.dict_from_keys(filtered_value, changed_fields)
            if bool(data_update) is False:
                return ApiResponse(
                    ResponseStatus.OK, StatusCode.status_200, data_update, None
                )
            # try:
            #     self.document_reference().set(data_update, merge=True)
            # except Exception as e:
            #     return ApiResponse(ResponseStatus.ERROR, StatusCode.status_400, None, Error(str(e),"INVALID_REQUEST", 1))
            # return DictUtils.dict_from_keys(self.get(), changed_fields);
            # fields = changed_fields
            print("data_update asset", data_update)
            return ApiResponse(
                ResponseStatus.OK,
                StatusCode.status_200,
                DictUtils.pick_fields(data_update, ASSETS_PICKABLE_FIELDS),
                None,
            )
        except Exception as e:
            print(e)
            return None

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