"""
Support for CouchDB 2.x.

:copyright: Copyright 2013-2017, Dorian Puła
    <dorian.pula@amber-penguin-software.ca>
:license: AGPL v3+
"""

import copy
import enum
import http
import json
import logging
import os
import typing
from urllib import parse
import uuid

import requests
import termcolor

EXTERNAL_DB_URI_PARAM = f'ROOKERIES_COUCHDB'
DEFAULT_DB_CONNECTION = 'http://admin:password@localhost:5984/rookeries'
logger = logging.getLogger(__name__)


class OpStatus(enum.Enum):
    SKIP = '[SKIP]', 'yellow'
    OK = '[ OK ]', 'green'
    FAILED = '[FAIL]', 'red'

    def __init__(self, output, color):
        self.output = output
        self.color = color

    def print(self):
        print(termcolor.colored(self.output, self.color))


class CouchDB(object):
    """
    Manages CouchDB connections and general operations.
    """

    def __init__(self, connection=None):

        if not connection:
            connection = DEFAULT_DB_CONNECTION

        # TODO: Create a validate connection string function.
        # TODO: Add tests.
        self.url = parse.urlparse(connection)
        self.protocol = f'{self.url.scheme}://'
        self.server = f'{self.url.hostname}:{self.url.port}'
        self.credentials = f'{self.url.username}:{self.url.password}'
        self.database = self.url.path.split('/')[-1]

    @staticmethod
    def configure_from_env():
        connection = os.environ.get(EXTERNAL_DB_URI_PARAM)
        return CouchDB(connection)

    def server_url(self):
        return f'{self.protocol}{self.credentials}@{self.server}'

    def connection(self):
        return self.url.geturl()

    def exists_db(self, database_name):
        db_exists = requests.get(f'{self.server_url()}/{database_name}')
        return db_exists.status_code == http.HTTPStatus.OK

    def create_db(self, database_name):
        logger.info(f'Creating database "{database_name}"')
        db_creation = requests.put(f'{self.server_url()}/{database_name}')
        status = db_creation.status_code == http.HTTPStatus.CREATED
        if status:
            logger.info('Created successfully')
        else:
            logger.warning(f'Could not create "{database_name}" database!')
        return status

    def find_all_documents(self, selector):
        response = requests.post(
            url=f'{self.connection()}/_find/',
            json={'selector': selector}
        )

        if response.status_code != http.HTTPStatus.OK:
            error_message = f'[{response.status_code}] {response.json()}'
            logger.error(f'Error searching for "{selector}": {error_message}')
            response.raise_for_status()

        doc_json = response.json()
        if doc_json.get('warning'):
            logger.warning(f'CouchDB warning: {doc_json["warning"]}')

        return doc_json['docs']

    def find_first_document(self, selector):
        documents = self.find_all_documents(selector)
        return None if len(documents) == 0 else documents[0]

    def get_document_by_id(self, doc_id, rev_id=None):
        headers = {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
        }

        if rev_id:
            headers['If-Match'] = rev_id

        response = requests.get(
            url=f'{self.connection()}/{doc_id}',
            headers=headers,
        )

        if response.status_code == http.HTTPStatus.NOT_FOUND:
            return None

        return response.json()

    def create_or_update_document(self, doc, doc_id=None, rev_id=None):
        if not doc_id:
            doc_id = uuid.uuid4()

        headers = {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
        }

        if rev_id:
            headers['If-Match'] = rev_id

        # TODO: Improve this code.
        response = requests.put(
            url=f'{self.connection()}/{doc_id}',
            json=doc,
            headers=headers
        )
        if response.status_code != http.HTTPStatus.CREATED:
            operation = 'create' if not rev_id else 'update'
            err = response.json()
            logger.error(f'Could not {operation} doc "{doc_id}": {err}')
            raise CouldNotUpdateDocError

        results = response.json()
        if rev_id:
            message = f'Updated "{results["id"]}" to revision {results["rev"]}'
        else:
            message = f'Created new document "{results["id"]}"'
        logger.info(message)


class CouldNotUpdateDocError(Exception):
    pass


class InvalidQueryError(Exception):
    pass


class NoMatchingDocumentFoundError(Exception):
    def __init__(
            self, db: CouchDB, query: typing.Union[dict, typing.Text]=None):
        self.db = db
        self.query = query

    def __str__(self):
        query_string = json.dumps(self.query, indent=4, sort_keys=True)
        db_url = self.db.connection()
        message = f'No document in "{db_url}" matches "{query_string}"'
        return message


# TODO: Test and document properly.
class DatabaseModel(object):
    def __init__(self, **kwargs):
        self._id = kwargs.get('_id')
        self._rev = kwargs.get('_rev')

    @staticmethod
    def doc_type():
        raise NotImplementedError

    @staticmethod
    def idenitifier():
        raise NotImplementedError

    @classmethod
    def find_in_db(cls, db: CouchDB, query: dict):
        # TODO: Add check if dict is serializable or not?
        if not isinstance(query, dict):
            raise InvalidQueryError

        doc_query = copy.deepcopy(query)
        doc_query['type'] = cls.doc_type()
        found_model = db.find_first_document(doc_query)

        if not found_model:
            raise NoMatchingDocumentFoundError(db, query)
        return cls(**found_model)

    @classmethod
    def get_in_db(cls, db: CouchDB, doc_id: str):
        found_model = db.get_document_by_id(doc_id)

        if not found_model:
            raise NoMatchingDocumentFoundError(db, f'ID {doc_id}')
        return cls(**found_model)

    def to_api_json(self):
        raise NotImplementedError

    def to_db_json(self):
        raise NotImplementedError

    def attach_db_metadata(self, db_json):
        if self._id:
            db_json['_id'] = self._id
        if self._rev:
            db_json['_rev'] = self._rev
        db_json['type'] = self.doc_type()
        return db_json
