import json
import time
from random import randint
import re
import warnings

from ..compat import (
    compat_urllib_error, compat_urllib_request,
    compat_http_client
)
from ..errors import ErrorHandler, ClientError, ClientConnectionError
from ..http import MultipartFormDataEncoder
from ..utils import (
    max_chunk_count_generator, max_chunk_size_generator,
    get_file_size
)
from ..compatpatch import ClientCompatPatch
from .common import ClientDeprecationWarning
from .common import MediaTypes
from socket import timeout, error as SocketError
from ssl import SSLError
try:
    ConnectionError = ConnectionError       # pylint: disable=redefined-builtin
except NameError:  # Python 2:
    class ConnectionError(Exception):
        pass


class MediaRatios(object):
    """
    Class holding valid aspect ratios (width: height) for media uploads.
    """

    # Based on IG sampling
    # and from https://help.instagram.com/1469029763400082
    #: Acceptable min, max values of with/height ratios for a standard media upload
    standard = 4.0 / 5.0, 90.0 / 47.0
    __device_ratios = [(3, 4), (2, 3), (5, 8), (3, 5), (9, 16), (10, 16), (40, 71)]
    __aspect_ratios = [1.0 * x[0] / x[1] for x in __device_ratios]

    #: Acceptable min, max values of with/height ratios for a story upload
    reel = min(__aspect_ratios), max(__aspect_ratios)


class UploadEndpointsMixin(object):
    """For endpoints relating to upload functionality."""

    EXTERNAL_LOC_SOURCES = {
        'foursquare': 'foursquare_v2_id',
        'facebook_places': 'facebook_places_id',
        'facebook_events': 'facebook_events_id'
    }

    def _validate_location(self, location):
        """
        Validates and patches a location dict for use with the upload functions

        :param location: dict containing location info
        :return:
        """
        location_keys = ['external_source', 'name', 'address']
        if not isinstance(location, dict):
            raise ValueError('Location must be a dict.')

        # patch location object returned from location_search
        if 'external_source' not in location and 'external_id_source' in location and 'external_id' in location:
            external_source = location['external_id_source']
            location['external_source'] = external_source
            if external_source in self.EXTERNAL_LOC_SOURCES:
                location[self.EXTERNAL_LOC_SOURCES[external_source]] = location['external_id']
        for k in location_keys:
            if not location.get(k):
                raise ValueError('Location dict must contain "{0!s}".'.format(k))
        for k, val in self.EXTERNAL_LOC_SOURCES.items():
            if location['external_source'] == k and not location.get(val):
                raise ValueError('Location dict must contain "{0!s}".'.format(val))

        media_loc = {
            'name': location['name'],
            'address': location['lat'],
            'external_source': location['external_source'],
        }
        if 'lat' in location and 'lng' in location:
            media_loc['lat'] = location['lat']
            media_loc['lng'] = location['lng']
        for k, val in self.EXTERNAL_LOC_SOURCES.items():
            if location['external_source'] == k:
                media_loc['external_source'] = k
                media_loc[val] = location[val]
        return media_loc

    @staticmethod
    def standard_ratios():  # pragma: no cover
        """
        Deprecated. Use MediaRatios.standard instead.
        Acceptable min, max values of with/height ratios for a standard media upload

        :return: tuple of (min. ratio, max. ratio)
        """
        warnings.warn(
            'Client.standard_ratios() is deprecated. '
            'Please use MediaRatios.standard instead.',
            ClientDeprecationWarning
        )
        return MediaRatios.standard

    @staticmethod
    def reel_ratios():  # pragma: no cover
        """
        Deprecated. Use MediaRatios.reel instead.
        Acceptable min, max values of with/height ratios for a story upload

        :return: tuple of (min. ratio, max. ratio)
        """
        warnings.warn(
            'Client.reel_ratios() is deprecated. '
            'Please use MediaRatios.reel instead.',
            ClientDeprecationWarning
        )
        return MediaRatios.reel

    @classmethod
    def compatible_aspect_ratio(cls, size):
        """
        Helper method to check aspect ratio for standard uploads

        :param size: tuple of (width, height)
        :return: True/False
        """
        min_ratio, max_ratio = MediaRatios.standard
        width, height = size
        this_ratio = 1.0 * width / height
        return min_ratio <= this_ratio <= max_ratio

    @classmethod
    def reel_compatible_aspect_ratio(cls, size):
        """
        Helper method to check aspect ratio for story uploads

        :param size: tuple of (width, height)
        :return: True/False
        """
        min_ratio, max_ratio = MediaRatios.reel
        width, height = size
        this_ratio = 1.0 * width / height
        return min_ratio <= this_ratio <= max_ratio

    def configure(self, upload_id, size, caption='', location=None,
                  disable_comments=False, is_sidecar=False):
        """
        Finalises a photo upload. This should not be called directly.
        Use :meth:`post_photo` instead.

        :param upload_id:
        :param size: tuple of (width, height)
        :param caption:
        :param location: a dict of venue/location information,
                         from :meth:`location_search` or :meth:`location_fb_search`
        :param disable_comments:
        :param is_sidecar: bool flag for album upload
        :return:
        """
        if not self.compatible_aspect_ratio(size):
            raise ValueError('Incompatible aspect ratio.')

        endpoint = 'media/configure/'
        width, height = size
        params = {
            'caption': caption,
            'media_folder': 'Instagram',
            'source_type': '4',
            'upload_id': upload_id,
            'device': {
                'manufacturer': self.phone_manufacturer,
                'model': self.phone_device,
                'android_version': self.android_version,
                'android_release': self.android_release
            },
            'edits': {
                'crop_original_size': [width * 1.0, height * 1.0],
                'crop_center': [0.0, -0.0],
                'crop_zoom': 1.0
            },
            'extra': {
                'source_width': width,
                'source_height': height,
            }
        }
        if location:
            media_loc = self._validate_location(location)
            params['location'] = json.dumps(media_loc)
            if 'lat' in location and 'lng' in location:
                params['geotag_enabled'] = '1'
                params['exif_latitude'] = '0.0'
                params['exif_longitude'] = '0.0'
                params['posting_latitude'] = str(location['lat'])
                params['posting_longitude'] = str(location['lng'])
                params['media_latitude'] = str(location['lat'])
                params['media_latitude'] = str(location['lng'])
        if disable_comments:
            params['disable_comments'] = '1'

        if is_sidecar:
            return params

        params.update(self.authenticated_params)
        res = self._call_api(endpoint, params=params)
        if self.auto_patch and res.get('media'):
            ClientCompatPatch.media(res.get('media'), drop_incompat_keys=self.drop_incompat_keys)
        return res

    def configure_video(self, upload_id, size, duration, thumbnail_data, caption='',
                        location=None, disable_comments=False, is_sidecar=False):
        """
        Finalises a video upload. This should not be called directly.
        Use :meth:`post_video` instead.

        :param upload_id:
        :param size: tuple of (width, height)
        :param duration: in seconds
        :param thumbnail_data: byte string of thumbnail photo
        :param caption:
        :param location: a dict of venue/location information,
                         from :meth:`location_search` or :meth:`location_fb_search`
        :param disable_comments:
        :param is_sidecar: bool flag for album upload
        :return:
        """
        if not self.compatible_aspect_ratio(size):
            raise ValueError('Incompatible aspect ratio.')

        # upload video thumbnail
        self.post_photo(thumbnail_data, size, caption, upload_id, location=location,
                        disable_comments=disable_comments, is_sidecar=is_sidecar)

        width, height = size
        params = {
            'upload_id': upload_id,
            'caption': caption,
            'source_type': '3',
            'poster_frame_index': 0,
            'length': duration * 1.0,
            'audio_muted': False,
            'filter_type': '0',
            'video_result': 'deprecated',
            'clips': {
                'length': duration * 1.0,
                'source_type': '3',
                'camera_position': 'back'
            },
            'device': {
                'manufacturer': self.phone_manufacturer,
                'model': self.phone_device,
                'android_version': self.android_version,
                'android_release': self.android_release
            },
            'extra': {
                'source_width': width,
                'source_height': height
            }
        }
        if disable_comments:
            params['disable_comments'] = '1'
        if location:
            media_loc = self._validate_location(location)
            params['location'] = json.dumps(media_loc)
            if 'lat' in location and 'lng' in location:
                params['geotag_enabled'] = '1'
                params['av_latitude'] = '0.0'
                params['av_longitude'] = '0.0'
                params['posting_latitude'] = str(location['lat'])
                params['posting_longitude'] = str(location['lng'])
                params['media_latitude'] = str(location['lat'])
                params['media_latitude'] = str(location['lng'])

        if is_sidecar:
            return params

        params.update(self.authenticated_params)
        res = self._call_api('media/configure/', params=params, query={'video': 1})
        if res.get('media') and self.auto_patch:
            ClientCompatPatch.media(res.get('media'), drop_incompat_keys=self.drop_incompat_keys)
        return res

    def configure_to_reel(self, upload_id, size):
        """
        Finalises a photo story upload. This should not be called directly.
        Use :meth:`post_photo_story` instead.

        :param upload_id:
        :param size: tuple of (width, height)
        :return:
        """
        if not self.reel_compatible_aspect_ratio(size):
            raise ValueError('Incompatible aspect ratio.')

        endpoint = 'media/configure_to_story/'
        width, height = size
        params = {
            'source_type': '4',
            'upload_id': upload_id,
            'story_media_creation_date': str(int(time.time()) - randint(11, 20)),
            'client_shared_at': str(int(time.time()) - randint(3, 10)),
            'client_timestamp': str(int(time.time())),
            'configure_mode': 1,      # 1 - REEL_SHARE, 2 - DIRECT_STORY_SHARE
            'device': {
                'manufacturer': self.phone_manufacturer,
                'model': self.phone_device,
                'android_version': self.android_version,
                'android_release': self.android_release
            },
            'edits': {
                'crop_original_size': [width * 1.0, height * 1.0],
                'crop_center': [0.0, 0.0],
                'crop_zoom': 1.3333334
            },
            'extra': {
                'source_width': width,
                'source_height': height,
            }
        }
        params.update(self.authenticated_params)
        res = self._call_api(endpoint, params=params)
        if self.auto_patch and res.get('media'):
            ClientCompatPatch.media(res.get('media'), drop_incompat_keys=self.drop_incompat_keys)
        return res

    def configure_video_to_reel(self, upload_id, size, duration, thumbnail_data):
        """
        Finalises a video story upload. This should not be called directly.
        Use :meth:`post_video_story` instead.

        :param upload_id:
        :param size: tuple of (width, height)
        :param duration: in seconds
        :param thumbnail_data: byte string of thumbnail photo
        :return:
        """
        if not self.reel_compatible_aspect_ratio(size):
            raise ValueError('Incompatible aspect ratio.')

        res = self.post_photo(thumbnail_data, size, '', upload_id=upload_id, to_reel=True)

        width, height = size
        params = {
            'source_type': '4',
            'upload_id': upload_id,
            'story_media_creation_date': str(int(time.time()) - randint(11, 20)),
            'client_shared_at': str(int(time.time()) - randint(3, 10)),
            'client_timestamp': str(int(time.time())),
            'configure_mode': 1,      # 1 - REEL_SHARE, 2 - DIRECT_STORY_SHARE
            'poster_frame_index': 0,
            'length': duration * 1.0,
            'audio_muted': False,
            'filter_type': '0',
            'video_result': 'deprecated',
            'clips': {
                'length': duration * 1.0,
                'source_type': '4',
                'camera_position': 'back'
            },
            'device': {
                'manufacturer': self.phone_manufacturer,
                'model': self.phone_device,
                'android_version': self.android_version,
                'android_release': self.android_release
            },
            'extra': {
                'source_width': width,
                'source_height': height,
            },
        }

        params.update(self.authenticated_params)
        res = self._call_api('media/configure_to_story/', params=params, query={'video': '1'})
        if self.auto_patch and res.get('media'):
            ClientCompatPatch.media(res.get('media'), drop_incompat_keys=self.drop_incompat_keys)
        return res

    def post_photo(self, photo_data, size, caption='', upload_id=None, to_reel=False, **kwargs):
        """
        Upload a photo.

        [CAUTION] FLAKY, IG is very finicky about sizes, etc, needs testing.

        :param photo_data: byte string of the image
        :param size: tuple of (width, height)
        :param caption:
        :param upload_id:
        :param to_reel: a Story photo
        :param kwargs:
            - **location**: a dict of venue/location information, from :meth:`location_search`
              or :meth:`location_fb_search`
            - **disable_comments**: bool to disable comments
        :return:
        """
        warnings.warn('This endpoint has not been fully tested.', UserWarning)

        # if upload_id is provided, it's a thumbnail for a vid upload
        for_video = True if upload_id else False

        if not for_video:
            if not to_reel and not self.compatible_aspect_ratio(size):
                raise ValueError('Incompatible aspect ratio.')
            if to_reel and not self.reel_compatible_aspect_ratio(size):
                raise ValueError('Incompatible reel aspect ratio.')
            if not 320 <= size[0] <= 1080:
                # range from https://help.instagram.com/1631821640426723
                raise ValueError('Invalid image width.')

        location = kwargs.pop('location', None)
        if location:
            self._validate_location(location)
        disable_comments = True if kwargs.pop('disable_comments', False) else False

        is_sidecar = kwargs.pop('is_sidecar', False)
        if not upload_id:
            upload_id = str(int(time.time() * 1000))

        endpoint = 'upload/photo/'
        fields = [
            ('upload_id', upload_id),
            ('_uuid', self.uuid),
            ('_csrftoken', self.csrftoken),
            ('image_compression', '{"lib_name":"jt","lib_version":"1.3.0","quality":"87"}')
        ]
        if is_sidecar:
            fields.append(('is_sidecar', '1'))
            if for_video:
                fields.append(('media_type', MediaTypes.VIDEO))

        files = [
            ('photo', 'pending_media_{0!s}{1!s}'.format(str(int(time.time() * 1000)), '.jpg'),
             'application/octet-stream', photo_data)
        ]

        content_type, body = MultipartFormDataEncoder().encode(fields, files)
        headers = self.default_headers
        headers['Content-Type'] = content_type
        headers['Content-Length'] = len(body)

        endpoint_url = '{0}{1}'.format(self.api_url.format(version='v1'), endpoint)
        req = compat_urllib_request.Request(endpoint_url, body, headers=headers)
        try:
            self.logger.debug('POST {0!s}'.format(endpoint_url))
            response = self.opener.open(req, timeout=self.timeout)
        except compat_urllib_error.HTTPError as e:
            error_response = self._read_response(e)
            self.logger.debug('RESPONSE: {0:d} {1!s}'.format(e.code, error_response))
            ErrorHandler.process(e, error_response)
        except (SSLError, timeout, SocketError,
                compat_urllib_error.URLError,   # URLError is base of HTTPError
                compat_http_client.HTTPException) as connection_error:
            raise ClientConnectionError('{} {}'.format(
                connection_error.__class__.__name__, str(connection_error)))

        post_response = self._read_response(response)
        self.logger.debug('RESPONSE: {0:d} {1!s}'.format(response.code, post_response))
        json_response = json.loads(post_response)

        if for_video and is_sidecar:
            return json_response

        upload_id = json_response['upload_id']

        # # NOTES: Logging traffic doesn't seem to indicate any additional "configure" after upload
        # # BUT not doing a "configure" causes a video post to fail with a
        # # "Other media configure error: b'yEmZkUpAj4'" error
        # if for_video:
        #     logger.debug('Skip photo configure.')
        #     return json_response
        if to_reel:
            return self.configure_to_reel(upload_id, size)
        else:
            return self.configure(upload_id, size, caption=caption, location=location,
                                  disable_comments=disable_comments, is_sidecar=is_sidecar)

    def post_video(self, video_data, size, duration, thumbnail_data, caption='', to_reel=False, **kwargs):
        """
        Upload a video

        [CAUTION] FLAKY, IG is very picky about sizes, etc, needs testing.

        :param video_data: byte string or a file-like object of the video content
        :param size: tuple of (width, height)
        :param duration: in seconds
        :param thumbnail_data: byte string of the video thumbnail content
        :param caption:
        :param to_reel: post to reel as Story
        :param kwargs:
             - **location**: a dict of venue/location information, from :meth:`location_search`
               or :meth:`location_fb_search`
             - **disable_comments**: bool to disable comments
             - **max_retry_count**: maximum attempts to reupload. Default 10.
        :return:
        """
        warnings.warn('This endpoint has not been fully tested.', UserWarning)

        if not to_reel and not self.compatible_aspect_ratio(size):
            raise ValueError('Incompatible aspect ratio.')

        if to_reel and not self.reel_compatible_aspect_ratio(size):
            raise ValueError('Incompatible reel aspect ratio.')

        if not 612 <= size[0] <= 1080:
            # range was determined through sampling of video uploads
            raise ValueError('Invalid video width.')

        if duration < 3.0:
            raise ValueError('Duration is less than 3s.')

        if not to_reel and duration > 60.0:
            raise ValueError('Duration is more than 60s.')

        if to_reel and duration > 15.0:
            raise ValueError('Duration is more than 15s.')

        max_file_len = 50 * 1024 * 1000
        try:
            video_file_len = len(video_data)
        except TypeError:
            video_file_len = get_file_size(video_data)
        if video_file_len > max_file_len:
            raise ValueError('Video file is too big.')

        location = kwargs.pop('location', None)
        if location:
            self._validate_location(location)
        disable_comments = True if kwargs.pop('disable_comments', False) else False

        endpoint = 'upload/video/'
        upload_id = str(int(time.time() * 1000))

        width, height = size
        params = {
            '_csrftoken': self.csrftoken,
            '_uuid': self.uuid,
            'upload_id': upload_id,
        }
        is_sidecar = kwargs.pop('is_sidecar', False)
        if is_sidecar:
            params['is_sidecar'] = '1'
        else:
            params.update({
                'media_type': MediaTypes.VIDEO,
                'upload_media_duration_ms': int(duration * 1000),
                'upload_media_width': width,
                'upload_media_height': height
            })

        res = self._call_api(endpoint, params=params, unsigned=True)
        upload_url = res['video_upload_urls'][-1]['url']
        upload_job = res['video_upload_urls'][-1]['job']

        successful_chunk_ranges = []
        all_done = False

        max_retry_count = kwargs.pop('max_retry_count', 10)
        configure_delay = 0
        for _ in range(max_retry_count + 1):

            try:
                # Prevent excessively small chunks
                if video_file_len > 1 * 1024 * 1000:
                    # max num of chunks = 4
                    chunk_generator = max_chunk_count_generator(4, video_data)
                else:
                    # max chunk size = 350,000 so that we'll always have
                    # <4 chunks when it's <1mb
                    chunk_generator = max_chunk_size_generator(350000, video_data)

                for chunk, data in chunk_generator:
                    skip_chunk = False
                    for received_chunk in successful_chunk_ranges:
                        if received_chunk[0] <= chunk.start and received_chunk[1] >= (chunk.end - 1):
                            skip_chunk = True
                            break
                    if skip_chunk:
                        self.logger.debug('Skipped chunk: {0:d} - {1:d}'.format(chunk.start, chunk.end - 1))
                        continue

                    headers = self.default_headers
                    headers['Connection'] = 'keep-alive'
                    headers['Content-Type'] = 'application/octet-stream'
                    headers['Content-Disposition'] = 'attachment; filename="video.mov"'
                    headers['Session-ID'] = upload_id
                    if is_sidecar:
                        headers['Cookie'] = 'sessionid=' + self.get_cookie_value('sessionid')
                    headers['job'] = upload_job
                    headers['Content-Length'] = chunk.length
                    headers['Content-Range'] = 'bytes {0:d}-{1:d}/{2:d}'.format(
                        chunk.start, chunk.end - 1, video_file_len)
                    self.logger.debug('POST {0!s}'.format(upload_url))
                    self.logger.debug('Uploading Content-Range: {0!s}'.format(headers['Content-Range']))

                    req = compat_urllib_request.Request(
                        str(upload_url), data=data, headers=headers)

                    try:
                        res = self.opener.open(req, timeout=self.timeout)
                        post_response = self._read_response(res)
                        self.logger.debug('RESPONSE: {0:d} {1!s}'.format(res.code, post_response))
                        if res.info().get('Content-Type', '').startswith('application/json'):
                            # last chunk
                            upload_res = json.loads(post_response)
                            configure_delay = int(upload_res.get('configure_delay_ms', 0)) / 1000.0
                            all_done = True
                            break
                        else:
                            successful_chunk_ranges = []
                            post_progress = post_response.split(',')
                            for progress in post_progress:
                                mobj = re.match(r'(?P<start>[0-9]+)\-(?P<end>[0-9]+)/(?P<total>[0-9]+)', progress)
                                if mobj:
                                    successful_chunk_ranges.append((int(mobj.group('start')), int(mobj.group('end'))))
                                else:
                                    self.logger.error(
                                        'Received unexpected chunk upload response: {0!s}'.format(post_response))
                                    raise ClientError(
                                        'Upload has failed due to unexpected upload response: {0!s}'.format(
                                            post_response),
                                        code=500)

                    except compat_urllib_error.HTTPError as e:
                        error_response = self._read_response(e)
                        self.logger.debug('RESPONSE: {0:d} {1!s}'.format(e.code, error_response))
                        ErrorHandler.process(e, error_response)

                    except (SSLError, timeout, SocketError,
                            compat_urllib_error.URLError,   # URLError is base of HTTPError
                            compat_http_client.HTTPException) as connection_error:
                        raise ClientConnectionError('{} {}'.format(
                            connection_error.__class__.__name__, str(connection_error)))

                else:
                    # if not break due to completed chunks then continue with next chunk
                    continue
                break

            except ClientConnectionError as cce:
                # connectivity prob, so let's just continue with another attempt
                self.logger.warning('ClientConnectionError posting chunks: {}'.format(
                    str(cce)))
                continue

        if not all_done:
            raise ClientError('Upload has failed due to incomplete chunk uploads.', code=500)

        if not configure_delay:
            configure_delay = 3
        configure_retry_max = 2

        for i in range(1, configure_retry_max + 1):
            try:
                if not to_reel:
                    result = self.configure_video(
                        upload_id, size, duration, thumbnail_data, caption=caption, location=location,
                        disable_comments=disable_comments, is_sidecar=is_sidecar)
                else:
                    result = self.configure_video_to_reel(
                        upload_id, size, duration, thumbnail_data)
                return result
            except ClientConnectionError as cce:
                if i < configure_retry_max:
                    self.logger.warning(
                        'Retry configure after {0:f} seconds: {1:s}'.format(
                            configure_delay, cce.msg))
                    time.sleep(configure_delay)
                else:
                    raise
            except ClientError as ce:
                if (ce.code == 202 or ce.msg == 'Transcode timeout') and i < configure_retry_max:
                    self.logger.warning('Retry configure after {0:f} seconds'.format(configure_delay))
                    time.sleep(configure_delay)
                else:
                    raise

    def post_photo_story(self, photo_data, size):
        """
        Upload a photo story

        :param photo_data: byte string of the image
        :param size: tuple of (width, height)
        :return:
        """
        return self.post_photo(
            photo_data=photo_data, size=size, to_reel=True)

    def post_video_story(self, video_data, size, duration, thumbnail_data):
        """
        Upload a video story

        :param video_data: byte string or a file-like object of the video content
        :param size: tuple of (width, height)
        :param duration: in seconds
        :param thumbnail_data: byte string of the video thumbnail content
        :return:
        """
        return self.post_video(
            video_data=video_data, size=size, duration=duration,
            thumbnail_data=thumbnail_data, to_reel=True)

    def post_album(self, medias, caption='', location=None, **kwargs):
        """
        Post an album of up to 10 photos/videos.

        :param medias: an iterable list/collection of media dict objects

            .. code-block:: javascript

                medias = [
                    {"type": "image", "size": (720, 720), "data": "..."},
                    {
                        "type": "image", "size": (720, 720),
                        "usertags": [{"user_id":4292127751, "position":[0.625347,0.4384531]}],
                        "data": "..."
                    },
                    {"type": "video", "size": (720, 720), "duration": 12.4, "thumbnail": "...", "data": "..."}
                ]

        :param caption:
        :param location:
        :return:
        """
        album_upload_id = str(int(time.time() * 1000))
        children_metadata = []
        for media in medias:
            if len(children_metadata) >= 10:
                continue
            if media.get('type', '') not in ['image', 'video']:
                raise ValueError('Invalid media type: {0!s}'.format(media.get('type', '')))
            if not media.get('data'):
                raise ValueError('Data not specified.')
            if not media.get('size'):
                raise ValueError('Size not specified.')
            if media['type'] == 'video':
                if not media.get('duration'):
                    raise ValueError('Duration not specified.')
                if not media.get('thumbnail'):
                    raise ValueError('Thumbnail not specified.')
            aspect_ratio = (media['size'][0] * 1.0) / (media['size'][1] * 1.0)
            if aspect_ratio > 1.0 or aspect_ratio < 1.0:
                raise ValueError('Invalid media aspect ratio.')

            if media['type'] == 'video':
                metadata = self.post_video(
                    video_data=media['data'],
                    size=media['size'],
                    duration=media['duration'],
                    thumbnail_data=media['thumbnail'],
                    is_sidecar=True
                )
            else:
                metadata = self.post_photo(
                    photo_data=media['data'],
                    size=media['size'],
                    is_sidecar=True,
                )
                if media.get('usertags'):
                    usertags = media['usertags']
                    utags = {'in': [{'user_id': str(u['user_id']), 'position': u['position']} for u in usertags]}
                    metadata['usertags'] = json.dumps(utags, separators=(',', ':'))
            children_metadata.append(metadata)

        if len(children_metadata) <= 1:
            raise ValueError('Invalid number of media objects: {0:d}'.format(len(children_metadata)))

        # configure as sidecar
        endpoint = 'media/configure_sidecar/'
        params = {
            'caption': caption,
            'client_sidecar_id': album_upload_id,
            'children_metadata': children_metadata
        }
        if location:
            media_loc = self._validate_location(location)
            params['location'] = json.dumps(media_loc)
            if 'lat' in location and 'lng' in location:
                params['geotag_enabled'] = '1'
                params['exif_latitude'] = '0.0'
                params['exif_longitude'] = '0.0'
                params['posting_latitude'] = str(location['lat'])
                params['posting_longitude'] = str(location['lng'])
                params['media_latitude'] = str(location['lat'])
                params['media_latitude'] = str(location['lng'])
        disable_comments = kwargs.pop('disable_comments', False)
        if disable_comments:
            params['disable_comments'] = '1'

        params.update(self.authenticated_params)
        res = self._call_api(endpoint, params=params)
        if self.auto_patch and res.get('media'):
            ClientCompatPatch.media(res.get('media'), drop_incompat_keys=self.drop_incompat_keys)
        return res
