class HrefField(Field):
    def __init__(self, read_only, name=None):
        super().__init__(name=name, read_only=read_only) class UuidField(Field):
    def __init__(self, read_only, name=None):
        super().__init__(name=name, read_only=read_only)

    def validate(self, value):
        value = super().validate(value)
        try:
            UUID(value, version=4)
            return value
        except ValueError:
            raise ValidationError(
                f'{value} is not a valid value for {type(self).__name__}'
            ) class StringField(Field):
    def __init__(self, read_only, name=None, max_length=None):
        super().__init__(name=name, read_only=read_only)
        self.max_length = max_length

    def validate(self, value):
        value = super().validate(value)
        if value and not isinstance(value, str):
            raise ValidationError(
                f'{value} is not a valid value for {type(self).__name__}'
            )
        if self.max_length is not None and len(value) > self.max_length:
            raise ValidationError(f'{self.name}: max length exceeded.')
        return value class BooleanField(Field):
    def __init__(self, read_only, name=None):
        super().__init__(name=name, read_only=read_only)

    def validate(self, value):
        if value and not isinstance(value, bool):
            raise ValidationError(
                '{value} is not a valid value for {type(self).__name__}'
            )
        return value class CompoundField(Field):
    def __init__(self, cls, read_only, name=None, validator=None):
        super().__init__(
            name=name, read_only=read_only, validator=validator
        )
        self.cls = cls

    def __get__(self, instance, owner):
        data = instance._data[self.name]
        # empty is used for read only fields, None all for others
        if data is not Field.EMPTY and data is not None:
            return self.cls(api=instance._api, _parent=instance, **data)
        else:
            return None class Resource(metaclass=ResourceMeta):
    """
    Resource is base class for all resources, hiding implementation details
    of magic of injecting instance of API and common operations (like generic
    query).
    """
    _API = None
    _URL = {}

    def __init__(self, api, *args, **kwargs):
        self.api = api

    @classmethod
    def _query(cls, **kwargs):
        """
        Generic query implementation that is used
        by the resources.
        """
        from sevenbridges.models.link import Link
        from sevenbridges.meta.collection import Collection

        api = kwargs.pop('api', cls._API)
        url = kwargs.pop('url')

        # Check for valid limit value
        if kwargs.get('limit') is not None and kwargs['limit'] <= 0:
            kwargs['limit'] = RequestParameters.DEFAULT_BULK_LIMIT

        extra = {'resource': cls.__name__, 'query': kwargs}
        logger.info('Querying %s resource', cls, extra=extra)
        response = api.get(url=url, params=kwargs)
        try:
            data = response.json()
        except JSONDecodeError:
            raise NonJSONResponseError(
                status=response.status_code,
                message=str(response.text)
            ) from None

        total = response.headers['x-total-matching-query']

        items = [cls(api=api, **item) for item in data['items']]
        links = [Link(**link) for link in data['links']]
        href = data['href']
        return Collection(
            resource=cls, href=href, total=total, items=items,
            links=links, api=api
        )

    @classmethod
    def get(cls, id, api=None):
        """
        Fetches the resource from the server.
        :param id: Resource identifier
        :param api: sevenbridges Api instance.
        :return: Resource object.
        """
        id = Transform.to_resource(id)
        api = api if api else cls._API
        if 'get' in cls._URL:
            extra = {'resource': cls.__name__, 'query': {'id': id}}
            logger.info('Fetching %s resource', cls, extra=extra)
            resource = api.get(url=cls._URL['get'].format(id=id)).json()
            return cls(api=api, **resource)
        else:
            raise SbgError('Unable to retrieve resource!')

    def delete(self):
        """
        Deletes the resource on the server.
        """
        if 'delete' in self._URL and hasattr(self, 'id'):
            extra = {'resource': type(self).__name__, 'query': {
                'id': self.id}}
            logger.info("Deleting %s resource.", self, extra=extra)
            self._api.delete(url=self._URL['delete'].format(id=self.id))
        else:
            raise SbgError('Resource can not be deleted!')

    def reload(self):
        """
        Refreshes the resource with the data from the server.
        """
        try:
            if hasattr(self, 'href'):
                data = self._api.get(self.href, append_base=False).json()
                resource = type(self)(api=self._api, **data)
            elif hasattr(self, 'id') and hasattr(self, '_URL') and \
                    'get' in self._URL:
                data = self._api.get(
                    self._URL['get'].format(id=self.id)).json()
                resource = type(self)(api=self._api, **data)
            else:
                raise SbgError(
                    'Resource can not be refreshed, "id" property not set or '
                    'retrieval for this resource is not available.'
                )
            query = {'id': self.id} if hasattr(self, 'id') else {}
            extra = {'resource': type(self).__name__, 'query': query}
            logger.info('Reloading %s resource.', self, extra=extra)
        except Exception as e:
            raise SbgError(
                f'Resource can not be refreshed due to an error: {e}'
            )

        self._data = resource._data
        self._dirty = resource._dirty
        self.update_old()
        return self

    def update_old(self):
        self._old = copy.deepcopy(self._data.data)

    def field(self, name):
        """
        Return field value if it's set
        :param name: Field name
        :return: Field value or None
        """
        return self._data.data.get(name, None)

    def _set(self, key, value):
        """
        Set property value in internal storage
        :param key: Property name
        :param value: Property value
        """
        self._data.data[key] = value class BillingGroupAnalysisBreakdown(Resource):
    _URL = {
        'query': '/billing/groups/{id}/breakdown/analysis'
    }

    project_name = StringField(read_only=True)
    analysis_app_name = StringField(read_only=True)
    analysis_name = StringField(read_only=True)
    analysis_type = StringField(read_only=True)
    analysis_id = UuidField(read_only=True)
    ran_by = StringField(read_only=True)
    analysis_status = StringField(read_only=True)
    analysis_cost = CompoundField(AnalysisCost, read_only=True)
    refunded_amount = FloatField(read_only=True)
    time_started = DateTimeField(read_only=True)
    time_finished = DateTimeField(read_only=True)
    project_locked = BooleanField(read_only=True)

    @classmethod
    def query(cls, bg_id, api=None, date_from=None, date_to=None,
              invoice_id=None, fields=None, offset=0, limit=50):
        """
        Query (List) billing group analysis breakdown. Date parameters must be
        string in format MM-DD-YYYY

        :param fields:
        :param invoice_id:
        :param date_to: include all analysis transactions charged before and
         including date_to
        :param date_from: include all analysis transactions charged after and
         including date_from
        :param bg_id: Billing Group ID
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :param api: Api instance.
        :return: Collection object.
        """
        api = api or cls._API

        return super(BillingGroupAnalysisBreakdown, cls)._query(
            url=cls._URL['query'].format(id=bg_id), offset=offset, limit=limit,
            date_from=date_from, date_to=date_to, invoice_id=invoice_id,
            fields=fields, api=api
        )

    def __str__(self):
        return '<BillingGroupAnalysisBreakdown>' class BillingGroupStorageBreakdown(Resource):
    _URL = {
        'query': '/billing/groups/{id}/breakdown/storage'
    }

    project_name = StringField(read_only=True)
    project_created_by = StringField(read_only=True)
    location = StringField(read_only=True)
    active = CompoundField(Measurement, read_only=True)
    archived = CompoundField(Measurement, read_only=True)
    project_locked = BooleanField(read_only=True)

    @classmethod
    def query(cls, bg_id, api=None, date_from=None, date_to=None,
              invoice_id=None, fields=None, offset=0, limit=50):
        """
        Query (List) billing group storage breakdown. Date parameters must be
        string in format MM-DD-YYYY

        :param fields:
        :param invoice_id:
        :param date_to: include all storage transactions charged before and
         including date_to
        :param date_from: include all storage transactions charged after and
         including date_from
        :param bg_id: Billing Group ID
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :param api: Api instance.
        :return: Collection object.
        """
        api = api or cls._API

        return super(BillingGroupStorageBreakdown, cls)._query(
            url=cls._URL['query'].format(id=bg_id), offset=offset, limit=limit,
            date_from=date_from, date_to=date_to, invoice_id=invoice_id,
            fields=fields, api=api
        )

    def __str__(self):
        return '<BillingGroupStorageBreakdown>' class BillingGroupEgressBreakdown(Resource):
    _URL = {
        'query': '/billing/groups/{id}/breakdown/egress'
    }

    project_name = StringField(read_only=True)
    downloaded_by = StringField(read_only=True)
    downloaded = CompoundField(Measurement, read_only=True)
    egress_cost = CompoundField(EgressCost, read_only=True)
    project_locked = BooleanField(read_only=True)

    @classmethod
    def query(cls, bg_id, api=None, date_from=None, date_to=None,
              invoice_id=None, fields=None, offset=0, limit=50):
        """
        Query (List) billing group egress breakdown. Date parameters must be
        string in format MM-DD-YYYY

        :param fields:
        :param invoice_id:
        :param date_to: include all egress transactions charged before and
         including date_to
        :param date_from: include all egress transactions charged after and
         including date_from
        :param bg_id: Billing Group ID
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :param api: Api instance.
        :return: Collection object.
        """
        api = api or cls._API

        return super(BillingGroupEgressBreakdown, cls)._query(
            url=cls._URL['query'].format(id=bg_id), offset=offset, limit=limit,
            date_from=date_from, date_to=date_to, invoice_id=invoice_id,
            fields=fields, api=api
        )

    def __str__(self):
        return '<BillingGroupEgressBreakdown>' class Price(Resource):
    """
    Price resource contains an information regarding the currency and the
    monet value of a certain resource.
    """
    currency = StringField(read_only=True)
    amount = FloatField(read_only=True)
    breakdown = CompoundField(Breakdown, read_only=True)

    def __str__(self):
        return f'<Price: currency={self.currency}, amount={self.amount}>'
from sevenbridges.meta.fields import (
    HrefField, UuidField, StringField, BooleanField, CompoundField
)
from sevenbridges.meta.resource import Resource
from sevenbridges.models.billing_analysis_breakdown import (
    BillingGroupAnalysisBreakdown
)
from sevenbridges.models.billing_storage_breakdown import (
    BillingGroupStorageBreakdown
)
from sevenbridges.models.billing_egress_breakdown import (
    BillingGroupEgressBreakdown
)
from sevenbridges.models.compound.price import Price


class BillingGroup(Resource):
    """
    Central resource for managing billing groups.
    """
    _URL = {
        'query': '/billing/groups',
        'get': '/billing/groups/{id}'
    }
    href = HrefField(read_only=True)

    id = UuidField(read_only=True)