import random
import re
import pathlib

from datetime import datetime

from bson.objectid import ObjectId
from pymongo import MongoClient

from flask_restful import (
    Resource,
    Api,
    request,
    abort,
)

from .app import app
from .exif import ExifTags
from .paginator import Paginator
from .parsers import (
    parse_query,
    query_parser,
)
from .power import RemotePower


api = Api(app)


client = MongoClient()
db = client.rusty_neutron


DEFAULT_DATE = datetime(1901, 1, 1)
DEFAULT_RATING = 50
DATE_FORMAT = '%Y-%m-%dT%H:%M:%S'
REGEX_DATE_AND_INDEX = r'\d{4}\.\d{2}\.\d{2}-\d{4}'


class PhotoMissingPath(Exception):
    pass


class ObjectDoesNotExist(Exception):
    pass


class ResourceFields(object):

    ID = 'id'
    MONGO_ID = '_id'


class PhotoFields(ResourceFields):

    PATH = 'path'
    RATING = 'rating'
    DATE = 'date'
    TITLE = 'title'
    ALBUM = 'album'


class SwitchFields(ResourceFields):

    NAME = 'name'
    STATE = 'state'
    LOCATION = 'location'
    REMOTE = 'remote'
    SOCKET = 'socket'


def get_resource_from_id(collection, resource_id):
    resource = collection.find_one({ResourceFields.MONGO_ID: ObjectId(resource_id)})
    if resource is None:
        abort(404)
    return resource


def get_photo_from_id(photo_id):
    return get_resource_from_id(db.photos, photo_id)


def get_random_photo(query):
    random_doc = random.randint(0, db.photos.count(query) - 1)
    return db.photos.find(query).limit(-1).skip(random_doc).next()


def delete_photo(photo):
    db.photos.delete_one(photo)


def update_photo(photo_id, photo_fields):
    db.photos.update(
        {ResourceFields.MONGO_ID: ObjectId(photo_id)},
        {'$set': photo_fields},
        upsert=False,
        multi=False,
    )


def rename_photo(photo, title):
    if photo[PhotoFields.TITLE] != title:
        path = photo[PhotoFields.PATH]
        date_and_index = re.search(
            r'({0})[^/]*.jpg$'.format(REGEX_DATE_AND_INDEX),
            path,
            re.IGNORECASE
        )
        if not date_and_index:
            abort(400, message='Photo cannot be renamed - unable to parse date and index')
        date_and_index = date_and_index.groups()[0]
        photo_file = pathlib.Path(path)
        new_name = '{0} - {1}{2}'.format(date_and_index, title, photo_file.suffix)
        new_path = photo_file.parent / new_name
        print('Renaming "{}" to "{}"'.format(photo_file, new_path))
        photo_file.rename(new_path)
        return str(new_path)


def rate_photo(photo, rating):
    if photo[PhotoFields.RATING] != rating:
        tags = ExifTags(pathlib.Path(photo[PhotoFields.PATH]))
        tags.rating = rating
        print('Rating "{}" as {}'.format(tags.path, rating))
        tags.write()


def dehydrate(resource):
    resource[ResourceFields.ID] = str(resource[ResourceFields.MONGO_ID])
    del resource[ResourceFields.MONGO_ID]
    return resource


def rehydrate(resource):
    try:
        resource[ResourceFields.MONGO_ID] = ObjectId(resource[ResourceFields.ID])
        del resource[ResourceFields.ID]
    except KeyError:
        pass
    return resource


def dehydrate_photo(photo):
    photo = dehydrate(photo)
    if photo[PhotoFields.DATE]:
        photo[PhotoFields.DATE] = photo[PhotoFields.DATE].strftime(DATE_FORMAT)
    return photo


def rehydrate_photo(photo, add_defaults=True):
    if PhotoFields.PATH not in photo and add_defaults:
        raise PhotoMissingPath()
    photo = rehydrate(photo)
    if add_defaults:
        photo[PhotoFields.DATE] = photo.get(PhotoFields.DATE)
        photo[PhotoFields.RATING] = int(photo.get(PhotoFields.RATING, DEFAULT_RATING))
        photo[PhotoFields.TITLE] = photo.get(PhotoFields.TITLE)
        photo[PhotoFields.ALBUM] = photo.get(PhotoFields.ALBUM)
    try:
        if photo[PhotoFields.DATE]:
            photo[PhotoFields.DATE] = datetime.strptime(photo[PhotoFields.DATE], DATE_FORMAT)
    except KeyError:
        pass
    return photo


class Photo(Resource):

    def get(self, photo_id):
        photo = get_photo_from_id(photo_id)
        return dehydrate_photo(photo)

    def put(self, photo_id):
        # TODO If rating, title, or path changes, update file.
        photo_new = rehydrate_photo(request.json)
        photo_old = get_photo_from_id(photo_id)
        if photo_old['path'] != photo_new['path']:
            abort(400, message='Not allowed to change file path via API')
        rate_photo(photo_old, photo_new[PhotoFields.RATING])
        new_path = rename_photo(photo_old, photo_new[PhotoFields.TITLE])
        if new_path:
            photo_new[PhotoFields.PATH] = new_path
        update_photo(photo_id, photo_new)

    def patch(self, photo_id):
        photo_new = rehydrate_photo(request.json, add_defaults=False)
        photo_old = get_photo_from_id(photo_id)
        if 'path' in photo_new and photo_new['path'] != photo_old['path']:
            abort(400, message='Not allowed to change file path via API')
        if PhotoFields.RATING in photo_new:
            rate_photo(photo_old, photo_new[PhotoFields.RATING])
        if PhotoFields.TITLE in photo_new:
            new_path = rename_photo(photo_old, photo_new[PhotoFields.TITLE])
            if new_path:
                photo_new[PhotoFields.PATH] = new_path
        update_photo(photo_id, photo_new)


def paginated_results(collection, dehydrate_fn):
    args = query_parser.parse_args()
    offset = args['offset']
    limit = min(1000, args['limit'])
    if limit <= 0:
        limit = 1000
    query = parse_query()
    objects = collection.find(query).skip(offset).limit(limit)
    total = collection.find(query).count()
    meta = Paginator(request.url, total, limit, offset).create_meta()
    return {
        'meta': meta,
        'objects': [dehydrate_fn(o) for o in objects]
    }


class PhotoList(Resource):

    def get(self):
        return paginated_results(db.photos, dehydrate_photo)


class Switch(Resource):

    @staticmethod
    def _flick(switch, state):
        socket = switch[SwitchFields.SOCKET]
        with RemotePower(switch[SwitchFields.REMOTE]) as switch:
            if state:
                switch.on(socket)
            else:
                switch.off(socket)

    def get(self, switch_id):
        switch = get_resource_from_id(db.switches, switch_id)
        return dehydrate(switch)

    def patch(self, switch_id):
        state_dict = rehydrate(request.json)
        switch = get_resource_from_id(db.switches, switch_id)
        if SwitchFields.STATE not in state_dict or len(state_dict) != 1:
            abort(400, message='Only allowed to change the state of the switches.')
        state = state_dict[SwitchFields.STATE]
        self._flick(switch, bool(state))
        db.switches.update(
            {ResourceFields.MONGO_ID: ObjectId(switch_id)},
            {'$set': {SwitchFields.STATE: state}},
            upsert=False,
            multi=False,
        )


class SwitchList(Resource):

    def get(self):
        return paginated_results(db.switches, dehydrate)


api.add_resource(Photo, '/api/photos/<photo_id>/')
api.add_resource(PhotoList, '/api/photos/')

api.add_resource(Switch, '/api/switches/<switch_id>/')
api.add_resource(SwitchList, '/api/switches/')
