from .snowflake import getclient, SnowflakeClient
from google.cloud import storage
from functools import lru_cache
import json
from collections import namedtuple
from socket import gethostname
from getpass import getuser
import time
import os
import pandas as pd

@lru_cache()
def _getblob_client(credentials):
    return storage.Client(credentials=credentials)

@lru_cache()
def _getblob_bucket(credentials, bucket_id, user_project):
    return _getblob_client(credentials).bucket(bucket_id, user_project)

TIMESTAMP_FORMAT = "%d/%m/%Y %H:%M:%S %Z" # always UTC

# hound/(entity)/(entity id)/(attribute) : For updates to entity attributes (update object)
# hound/(entity)/(entity id)/__meta__ : For updates to entities themselves (creation/deletion) (log object)
# hound/workspace/(attribute) : For updates to workspace attributes (update object)
# hound/workspace/__meta__ : For updates to workspace itself (creation/acl) (log object)
# hound/logs/{job,upload} : For logs of when jobs or uploads were performed (log object)
# hound/logs/other : For arbitrary user-generated log entries (log object)
# hound/logs/meta : For meta-logs, generated by hound itself (log object)

LogEntry = namedtuple("LogEntry", ['entities', 'timestamp', 'author', 'text'])
AttributeUpdate = namedtuple("AttributeUpdate", ['entityType', 'entityName', 'attributeName', 'attributeValue', 'timestamp', 'updateReason', 'author'])

class HoundClient(object):
    def __init__(self, bucket_id, snowflake_client='__COMMON__', credentials=None, user_project=None):
        """
        Constructs a new Hound Client
        Must provide bucket_id of firecloud workspace

        snowflake_client: The client or client name to use for generating snowflakes
        If left out, a default client will be used

        credentials: Google Cloud credentials object to use if not using application
        default credentials

        user_project: string project id to bill for usage, if using a requester-pays bucket
        """

        self.bucket = _getblob_bucket(credentials, bucket_id, user_project)
        self.snowflake_client = (
            snowflake_client if isinstance(snowflake_client, SnowflakeClient)
            else getclient(snowflake_client)
        )
        self.author = '{}@{}'.format(getuser(), gethostname())

    def write(self, path, data):
        """
        Low-level write
        Posts the given object to the path
        Object may be a JSON dictionary, LogEntry, or AttributeUpdate
        Timestamp may be omitted or None, which will be filled automatically
        Author may be omitted or None, which will be filled automatically
        """
        if isinstance(data, LogEntry):
            data = {
                'entities': data.entities,
                'timestamp': data.timestamp,
                'author': data.author,
                'text': data.text
            }
        elif isinstance(data, AttributeUpdate):
            data = {
                'entityType': data.entityType,
                'entityName': data.entityName,
                'attributeName': data.attributeName,
                'attributeValue': data.attributeValue,
                'timestamp': data.timestamp,
                'updateReason': data.updateReason,
                'author': data.author
            }
        if 'author' not in data or data['author'] is None:
            data['author'] = self.author
        if 'timestamp' not in data or data['timestamp'] is None:
            data['timestamp'] = time.strftime(TIMESTAMP_FORMAT, time.gmtime())
        path = os.path.join(path, self.snowflake_client.snowflake().hex())
        blob = storage.Blob(path, self.bucket)
        blob.upload_from_string(json.dumps(data))
        return blob

    def update_entity_attribute(self, etype, entity, attribute, value, reason=None):
        """
        Log an update to entity attributes
        etype: entity type
        entity: entity name
        attribute: attribute name
        value: value attribute
        reason: (optional) reason attribute was updated

        Also adds an entry to the meta log
        returns blob for entity log
        """
        blob = self.write(
            os.path.join('hound', etype, entity, attribute),
            {
                'entityType': etype,
                'entityName': entity,
                'attributeName': attribute,
                'attributeValue': value,
                'updateReason': reason if reason is not None else "No reason given"
            }
        )
        self.write(
            'hound/logs/meta',
            {
                'entities': [os.path.join(etype, entity)],
                'text': 'snowflake={}; Updated attribute: {}'.format(
                    os.path.basename(blob.name),
                    attribute
                )
            }
        )
        return blob

    def update_entity_meta(self, etype, entity, event, reason=None):
        """
        Log a creation, deletion, or other meta-event on an entity
        etype: entity type
        entity: entity id
        event: what occurred
        reason: (optional) reason the event occurred

        Also adds an entry to the meta log
        returns blob for entity log
        """
        blob = self.write(
            os.path.join('hound', etype, entity, '__meta__'),
            {
                'entityType': etype,
                'entityName': entity,
                'attributeName': '__meta__',
                'attributeValue': event,
                'updateReason': reason if reason is not None else "No reason given"
            }
        )
        self.write(
            'hound/logs/meta',
            {
                'entities': [os.path.join(etype, entity)],
                'text': 'snowflake={}; Add/Removed entity (meta-event)'.format(
                    os.path.basename(blob.name),
                )
            }
        )
        return blob

    def update_workspace_attribute(self, attribute, value, reason=None):
        """
        Log an update to workspace attributes
        attribute: attribute name
        value: value attribute
        reason: (optional) reason attribute was updated

        Also adds an entry to the meta log
        returns blob for entity log
        """
        blob = self.write(
            os.path.join('hound', 'workspace', attribute),
            {
                'entityType': 'workspace',
                'entityName': 'workspace',
                'attributeName': attribute,
                'attributeValue': value,
                'updateReason': reason if reason is not None else "No reason given"
            }
        )
        self.write(
            'hound/logs/meta',
            {
                'entities': ['workspace'],
                'text': 'snowflake={}; Updated attribute: {}'.format(
                    os.path.basename(blob.name),
                    attribute
                )
            }
        )
        return blob

    def update_workspace_meta(self, event, reason=None):
        """
        Log a creation, deletion, or other meta-event on the workspace
        event: what occurred
        reason: (optional) reason the event occurred

        Also adds an entry to the meta log
        returns blob for entity log
        """
        blob = self.write(
            'hound/workspace/__meta__',
            {
                'entityType': 'workspace',
                'entityName': 'workspace',
                'attributeName': '__meta__',
                'attributeValue': event,
                'updateReason': reason if reason is not None else "No reason given"
            }
        )
        self.write(
            'hound/logs/meta',
            {
                'entities': ['workspace'],
                'text': 'snowflake={}; Add/Removed entity (meta-event)'.format(
                    os.path.basename(blob.name),
                )
            }
        )
        return blob

    def write_log_entry(self, log_type, text, entities=None):
        """
        Write a new entry to the log
        log_type must be one of {'job', 'upload', 'other'}

        text: Log text
        entities: (optional) list of entities relevant to this log entry
        For consistency, entity names should be in format "entity_type/entity_name"

        Also adds an entry to the meta log
        returns blob for entity log
        """
        if log_type not in {'job', 'upload', 'other'}:
            raise TypeError("log_type must be in {'job', 'upload', 'other'}")
        blob = self.write(
            os.path.join('hound', 'logs', log_type),
            {
                'entities': entities,
                'text': text
            }
        )
        self.write(
            'hound/logs/meta',
            {
                'entities': [os.path.join('logs', log_type)],
                'text': 'snowflake={}; Added entry to "{}" log'.format(
                    os.path.basename(blob.name),
                    log_type
                )
            }
        )
        return blob

    def get_entries(self, path):
        """
        Iterates over log entires at a given path
        """
        for page in self.bucket.list_blobs(prefix=path).pages:
            yield from page

    def _yield_logs(self, src):
        for entry in src:
            data = json.loads(entry.download_as_string())
            yield LogEntry(data['entities'], data['timestamp'], data['author'], data['text'])

    def _yield_updates(self, src):
        for entry in src:
            data = json.loads(entry.download_as_string())
            yield AttributeUpdate(
                data['entityType'],
                data['entityName'],
                data['attributeName'],
                data['attributeValue'],
                data['timestamp'],
                data['updateReason'],
                data['author']
            )

    def latest(self, src, entryType=None):
        """
        Returns the most recent object from a source iterator
        src: google.cloud.storage.bucket_iterator (as returned by HoundClient.get_entries) OR string hound path (ie: "hound/workspace/foo")
        entryType: (optional) Return the latest object as a parsed object type instead of a blob
        {entryType may be "logs" or "updates"}

        Returns the latest object from the given iterator or None
        """
        if isinstance(src, list):
            src = iter(src)
        if isinstance(src, str):
            src = self.get_entries(src)
        if entryType is not None:
            if entryType == 'logs':
                entryType = lambda x: [*self._yield_logs((x,))][0]
            elif entryType == 'updates':
                entryType = lambda x: [*self._yield_updates((x,))][0]
            else:
                raise TypeError("entryType must be in {'logs', 'updates'}")
        latest = None
        latest_blob = None
        for entry in src:
            snowflake = self.snowflake_client.unpack(os.path.basename(entry.name))
            if latest is None or snowflake.time > latest.time:
                latest = snowflake
                latest_blob = entry
        return latest_blob if entryType is None else entryType(latest_blob)

    def iread_log(self, log_type, since=None):
        """
        Iterates over log entries from the requested log type
        log_type must be one of: 'job', 'upload', 'other', 'meta'
        Since should be a unit timestamp or python struct_time object (UTC)
        Only logs since that value will be returned
        Probably ordered, but this is not guaranteed by Google
        """
        if log_type not in {'job', 'upload', 'other', 'meta'}:
            raise TypeError("log_type must be one of {'job', 'upload', 'other', 'meta'}")
        if isinstance(since, (float, int)):
            since = time.gmtime(since)
        for entry in self._yield_logs(self.get_entries('hound/logs/{}/'.format(log_type))):
            if since is None or since <= time.strptime(entry.timestamp, TIMESTAMP_FORMAT):
                yield entry

    def read_log(self, log_type, since=None):
        """
        Returns a list of entries of the requested log type
        log_type must be one of: 'job', 'upload', 'other', 'meta'
        Since should be a unit timestamp or python struct_time object (UTC)
        Only logs since that value will be returned
        Sorted by timestamp from oldest to most recent
        """
        return sorted(
            self.iread_log(log_type, since),
            key=lambda entry: time.strptime(entry.timestamp, TIMESTAMP_FORMAT)
        )

    def ientity_attribute_provenance(self, etype, entity=None, attribute=None):
        """
        Iterates over attribute updates for the requested enity type
        etype: Entity type
        entity: (optional) entity name
        attribute: (optional) attribute

        If entity is not given, iterate over all entities
        If attribute is not given, iterate over all attributes
        Probably ordered, but this is not guaranteed by Google
        """
        if entity is not None and attribute is None:
            source = self.get_entries(os.path.join('hound', etype, entity))
        elif entity is None or attribute is None:
            source = self.get_entries(os.path.join('hound', etype))
        elif entity is not None and attribute is not None:
            source = self.get_entries(os.path.join('hound', etype, entity, attribute))
        for entry in self._yield_updates(source):
            if (attribute is None and entity.attributeName != '__meta__') or entry.attributeName == attribute:
                yield entry


    def iworkspace_attribute_provenance(self, attribute=None):
        """
        Iterates over attribute updates for the workspace
        attribute: (optional) attribute name

        If attribute is not given, iterate over all attributes
        Probably ordered, but this is not guaranteed by Google
        """
        if attribute is None:
            source = self.get_entries('hound/workspace')
        else:
            source = self.get_entries(os.path.join('hound', 'workspace', attribute))
        for entry in self._yield_updates(source):
            if (attribute is None and entity.attributeName != '__meta__') or entity.attributeName == attribute:
                yield entry

    def entity_attribute_provenance(self, etype, entity=None, attribute=None):
        """
        Returns a list of attribute updates for the requested enity type
        etype: Entity type
        entity: (optional) entity name
        attribute: (optional) attribute

        If entity is not given, iterate over all entities
        If attribute is not given, iterate over all attributes
        Sorted from oldest to most recent
        """
        return sorted(
            self.ientity_attribute_provenance(etype, entity, attribute),
            key=lambda entry: time.strptime(entry.timestamp, TIMESTAMP_FORMAT)
        )

    def workspace_attribute_provenance(self, attribute=None):
        """
        Returns a list of attribute updates for the workspace
        attribute: (optional) attribute name

        If attribute is not given, iterate over all attributes
        Probably ordered, but this is not guaranteed by Google
        """
        return sorted(
            self.iworkspace_attribute_provenance(attribute),
            key=lambda entry: time.strptime(entry.timestamp, TIMESTAMP_FORMAT)
        )

    def record_entity_upload(self, etype, data, entity_id=None, reason=None):
        """
        Record an entity upload
        etype: entity type
        data: dict or pandas series containing entity metadata
        entity_id: (optional) id if data is a dictionary or the series index is not the id
        reason: (optional) Reason why the entity is being uploaded

        Actions:
        1) Writes to upload log
        2) Writes to entity meta log (entity creation)
        3) Writes to entity attribute log (for each attribute)
        """
        if entity_id is None:
            entity_id = data.name
        self.write_log_entry('upload', 'Uploading new entity', [os.path.join(etype, entity_id)])
        self.update_entity_meta(etype, entity_id, "User uploaded new entity", reason)
        for attribute, value in data.items():
            self.update_entity_attribute(etype, entity_id, attribute, value, reason)
