import collections
import uuid
from datetime import datetime
from queue import Queue

from bson.objectid import ObjectId
from pype.modules.ftrack.lib import BaseAction, statics_icon
from avalon.api import AvalonMongoDB


class DeleteAssetSubset(BaseAction):
    '''Edit meta data action.'''

    #: Action identifier.
    identifier = "delete.asset.subset"
    #: Action label.
    label = "Delete Asset/Subsets"
    #: Action description.
    description = "Removes from Avalon with all childs and asset from Ftrack"
    icon = statics_icon("ftrack", "action_icons", "DeleteAsset.svg")

    settings_key = "delete_asset_subset"
    #: Db connection
    dbcon = AvalonMongoDB()

    splitter = {"type": "label", "value": "---"}
    action_data_by_id = {}
    asset_prefix = "asset:"
    subset_prefix = "subset:"

    def discover(self, session, entities, event):
        """ Validation """
        task_ids = []
        for ent_info in event["data"]["selection"]:
            if ent_info.get("entityType") == "task":
                task_ids.append(ent_info["entityId"])

        is_valid = False
        for entity in entities:
            if (
                entity["id"] in task_ids
                and entity.entity_type.lower() != "task"
            ):
                is_valid = True
                break

        if is_valid:
            is_valid = self.valid_roles(session, entities, event)
        return is_valid

    def _launch(self, event):
        try:
            entities = self._translate_event(event)
            if "values" not in event["data"]:
                self.dbcon.install()
                return self._interface(self.session, entities, event)

            confirmation = self.confirm_delete(entities, event)
            if confirmation:
                return confirmation

            self.dbcon.install()
            response = self.launch(
                self.session, entities, event
            )
        finally:
            self.dbcon.uninstall()

        return self._handle_result(response)

    def interface(self, session, entities, event):
        self.show_message(event, "Preparing data...", True)
        items = []
        title = "Choose items to delete"

        # Filter selection and get ftrack ids
        selection = event["data"].get("selection") or []
        ftrack_ids = []
        project_in_selection = False
        for entity in selection:
            entity_type = (entity.get("entityType") or "").lower()
            if entity_type != "task":
                if entity_type == "show":
                    project_in_selection = True
                continue

            ftrack_id = entity.get("entityId")
            if not ftrack_id:
                continue

            ftrack_ids.append(ftrack_id)

        if project_in_selection:
            msg = "It is not possible to use this action on project entity."
            self.show_message(event, msg, True)

        # Filter event even more (skip task entities)
        # - task entities are not relevant for avalon
        entity_mapping = {}
        for entity in entities:
            ftrack_id = entity["id"]
            if ftrack_id not in ftrack_ids:
                continue

            if entity.entity_type.lower() == "task":
                ftrack_ids.remove(ftrack_id)

            entity_mapping[ftrack_id] = entity

        if not ftrack_ids:
            # It is bug if this happens!
            return {
                "success": False,
                "message": "Invalid selection for this action (Bug)"
            }

        if entities[0].entity_type.lower() == "project":
            project = entities[0]
        else:
            project = entities[0]["project"]

        project_name = project["full_name"]
        self.dbcon.Session["AVALON_PROJECT"] = project_name

        selected_av_entities = list(self.dbcon.find({
            "type": "asset",
            "data.ftrackId": {"$in": ftrack_ids}
        }))
        found_without_ftrack_id = {}
        if len(selected_av_entities) != len(ftrack_ids):
            found_ftrack_ids = [
                ent["data"]["ftrackId"] for ent in selected_av_entities
            ]
            for ftrack_id, entity in entity_mapping.items():
                if ftrack_id in found_ftrack_ids:
                    continue

                av_ents_by_name = list(self.dbcon.find({
                    "type": "asset",
                    "name": entity["name"]
                }))
                if not av_ents_by_name:
                    continue

                ent_path_items = [ent["name"] for ent in entity["link"]]
                parents = ent_path_items[1:len(ent_path_items)-1:]
                # TODO we should say to user that
                # few of them are missing in avalon
                for av_ent in av_ents_by_name:
                    if av_ent["data"]["parents"] != parents:
                        continue

                    # TODO we should say to user that found entity
                    # with same name does not match same ftrack id?
                    if "ftrackId" not in av_ent["data"]:
                        selected_av_entities.append(av_ent)
                        found_without_ftrack_id[str(av_ent["_id"])] = ftrack_id
                        break

        if not selected_av_entities:
            return {
                "success": False,
                "message": "Didn't found entities in avalon"
            }

        # Remove cached action older than 2 minutes
        old_action_ids = []
        for id, data in self.action_data_by_id.items():
            created_at = data.get("created_at")
            if not created_at:
                old_action_ids.append(id)
                continue
            cur_time = datetime.now()
            existing_in_sec = (created_at - cur_time).total_seconds()
            if existing_in_sec > 60 * 2:
                old_action_ids.append(id)

        for id in old_action_ids:
            self.action_data_by_id.pop(id, None)

        # Store data for action id
        action_id = str(uuid.uuid1())
        self.action_data_by_id[action_id] = {
            "attempt": 1,
            "created_at": datetime.now(),
            "project_name": project_name,
            "subset_ids_by_name": {},
            "subset_ids_by_parent": {},
            "without_ftrack_id": found_without_ftrack_id
        }

        id_item = {
            "type": "hidden",
            "name": "action_id",
            "value": action_id
        }

        items.append(id_item)
        asset_ids = [ent["_id"] for ent in selected_av_entities]
        subsets_for_selection = self.dbcon.find({
            "type": "subset",
            "parent": {"$in": asset_ids}
        })

        asset_ending = ""
        if len(selected_av_entities) > 1:
            asset_ending = "s"

        asset_title = {
            "type": "label",
            "value": "# Delete asset{}:".format(asset_ending)
        }
        asset_note = {
            "type": "label",
            "value": (
                "<p><i>NOTE: Action will delete checked entities"
                " in Ftrack and Avalon with all children entities and"
                " published content.</i></p>"
            )
        }

        items.append(asset_title)
        items.append(asset_note)

        asset_items = collections.defaultdict(list)
        for asset in selected_av_entities:
            ent_path_items = [project_name]
            ent_path_items.extend(asset.get("data", {}).get("parents") or [])
            ent_path_to_parent = "/".join(ent_path_items) + "/"
            asset_items[ent_path_to_parent].append(asset)

        for asset_parent_path, assets in sorted(asset_items.items()):
            items.append({
                "type": "label",
                "value": "## <b>- {}</b>".format(asset_parent_path)
            })
            for asset in assets:
                items.append({
                    "label": asset["name"],
                    "name": "{}{}".format(
                        self.asset_prefix, str(asset["_id"])
                    ),
                    "type": 'boolean',
                    "value": False
                })

        subset_ids_by_name = collections.defaultdict(list)
        subset_ids_by_parent = collections.defaultdict(list)
        for subset in subsets_for_selection:
            subset_id = subset["_id"]
            name = subset["name"]
            parent_id = subset["parent"]
            subset_ids_by_name[name].append(subset_id)
            subset_ids_by_parent[parent_id].append(subset_id)

        if not subset_ids_by_name:
            return {
                "items": items,
                "title": title
            }

        subset_ending = ""
        if len(subset_ids_by_name.keys()) > 1:
            subset_ending = "s"

        subset_title = {
            "type": "label",
            "value": "# Subset{} to delete:".format(subset_ending)
        }
        subset_note = {
            "type": "label",
            "value": (
                "<p><i>WARNING: Subset{} will be removed"
                " for all <b>selected</b> entities.</i></p>"
            ).format(subset_ending)
        }

        items.append(self.splitter)
        items.append(subset_title)
        items.append(subset_note)

        for name in subset_ids_by_name:
            items.append({
                "label": "<b>{}</b>".format(name),
                "name": "{}{}".format(self.subset_prefix, name),
                "type": "boolean",
                "value": False
            })

        self.action_data_by_id[action_id]["subset_ids_by_parent"] = (
            subset_ids_by_parent
        )
        self.action_data_by_id[action_id]["subset_ids_by_name"] = (
            subset_ids_by_name
        )

        return {
            "items": items,
            "title": title
        }

    def confirm_delete(self, entities, event):
        values = event["data"]["values"]
        action_id = values.get("action_id")
        spec_data = self.action_data_by_id.get(action_id)
        if not spec_data:
            # it is a bug if this happens!
            return {
                "success": False,
                "message": "Something bad has happened. Please try again."
            }

        # Process Delete confirmation
        delete_key = values.get("delete_key")
        if delete_key:
            delete_key = delete_key.lower().strip()
            # Go to launch part if user entered `delete`
            if delete_key == "delete":
                return
            # Skip whole process if user didn't enter any text
            elif delete_key == "":
                self.action_data_by_id.pop(action_id, None)
                return {
                    "success": True,
                    "message": "Deleting cancelled (delete entry was empty)"
                }
            # Get data to show again
            to_delete = spec_data["to_delete"]

        else:
            to_delete = collections.defaultdict(list)
            for key, value in values.items():
                if not value:
                    continue
                if key.startswith(self.asset_prefix):
                    _key = key.replace(self.asset_prefix, "")
                    to_delete["assets"].append(_key)

                elif key.startswith(self.subset_prefix):
                    _key = key.replace(self.subset_prefix, "")
                    to_delete["subsets"].append(_key)

            self.action_data_by_id[action_id]["to_delete"] = to_delete

        asset_to_delete = len(to_delete.get("assets") or []) > 0
        subset_to_delete = len(to_delete.get("subsets") or []) > 0

        if not asset_to_delete and not subset_to_delete:
            self.action_data_by_id.pop(action_id, None)
            return {
                "success": True,
                "message": "Nothing was selected to delete"
            }

        attempt = spec_data["attempt"]
        if attempt > 3:
            self.action_data_by_id.pop(action_id, None)
            return {
                "success": False,
                "message": "You didn't enter \"DELETE\" properly 3 times!"
            }

        self.action_data_by_id[action_id]["attempt"] += 1

        title = "Confirmation of deleting"

        if asset_to_delete:
            asset_len = len(to_delete["assets"])
            asset_ending = ""
            if asset_len > 1:
                asset_ending = "s"
            title += " {} Asset{}".format(asset_len, asset_ending)
            if subset_to_delete:
                title += " and"

        if subset_to_delete:
            sub_len = len(to_delete["subsets"])
            type_ending = ""
            sub_ending = ""
            if sub_len == 1:
                subset_ids_by_name = spec_data["subset_ids_by_name"]
                if len(subset_ids_by_name[to_delete["subsets"][0]]) > 1:
                    sub_ending = "s"

            elif sub_len > 1:
                type_ending = "s"
                sub_ending = "s"

            title += " {} type{} of subset{}".format(
                sub_len, type_ending, sub_ending
            )

        items = []

        id_item = {"type": "hidden", "name": "action_id", "value": action_id}
        delete_label = {
            'type': 'label',
            'value': '# Please enter "DELETE" to confirm #'
        }
        delete_item = {
            "name": "delete_key",
            "type": "text",
            "value": "",
            "empty_text": "Type Delete here..."
        }

        items.append(id_item)
        items.append(delete_label)
        items.append(delete_item)

        return {
            "items": items,
            "title": title
        }

    def launch(self, session, entities, event):
        self.show_message(event, "Processing...", True)
        values = event["data"]["values"]
        action_id = values.get("action_id")
        spec_data = self.action_data_by_id.get(action_id)
        if not spec_data:
            # it is a bug if this happens!
            return {
                "success": False,
                "message": "Something bad has happened. Please try again."
            }

        report_messages = collections.defaultdict(list)

        project_name = spec_data["project_name"]
        to_delete = spec_data["to_delete"]
        self.dbcon.Session["AVALON_PROJECT"] = project_name

        assets_to_delete = to_delete.get("assets") or []
        subsets_to_delete = to_delete.get("subsets") or []

        # Convert asset ids to ObjectId obj
        assets_to_delete = [ObjectId(id) for id in assets_to_delete if id]

        subset_ids_by_parent = spec_data["subset_ids_by_parent"]
        subset_ids_by_name = spec_data["subset_ids_by_name"]

        subset_ids_to_archive = []
        asset_ids_to_archive = []
        ftrack_ids_to_delete = []
        if len(assets_to_delete) > 0:
            map_av_ftrack_id = spec_data["without_ftrack_id"]
            # Prepare data when deleting whole avalon asset
            avalon_assets = self.dbcon.find({"type": "asset"})
            avalon_assets_by_parent = collections.defaultdict(list)
            for asset in avalon_assets:
                asset_id = asset["_id"]
                parent_id = asset["data"]["visualParent"]
                avalon_assets_by_parent[parent_id].append(asset)
                if asset_id in assets_to_delete:
                    ftrack_id = map_av_ftrack_id.get(str(asset_id))
                    if not ftrack_id:
                        ftrack_id = asset["data"].get("ftrackId")

                    if not ftrack_id:
                        continue
                    ftrack_ids_to_delete.append(ftrack_id)

            children_queue = Queue()
            for mongo_id in assets_to_delete:
                children_queue.put(mongo_id)

            while not children_queue.empty():
                mongo_id = children_queue.get()
                if mongo_id in asset_ids_to_archive:
                    continue

                asset_ids_to_archive.append(mongo_id)
                for subset_id in subset_ids_by_parent.get(mongo_id, []):
                    if subset_id not in subset_ids_to_archive:
                        subset_ids_to_archive.append(subset_id)

                children = avalon_assets_by_parent.get(mongo_id)
                if not children:
                    continue

                for child in children:
                    child_id = child["_id"]
                    if child_id not in asset_ids_to_archive:
                        children_queue.put(child_id)

        # Prepare names of assets in ftrack and ids of subsets in mongo
        asset_names_to_delete = []
        if len(subsets_to_delete) > 0:
            for name in subsets_to_delete:
                asset_names_to_delete.append(name)
                for subset_id in subset_ids_by_name[name]:
                    if subset_id in subset_ids_to_archive:
                        continue
                    subset_ids_to_archive.append(subset_id)

        # Get ftrack ids of entities where will be delete only asset
        not_deleted_entities_id = []
        ftrack_id_name_map = {}
        if asset_names_to_delete:
            for entity in entities:
                ftrack_id = entity["id"]
                ftrack_id_name_map[ftrack_id] = entity["name"]
                if ftrack_id not in ftrack_ids_to_delete:
                    not_deleted_entities_id.append(ftrack_id)

        mongo_proc_txt = "MongoProcessing: "
        ftrack_proc_txt = "Ftrack processing: "
        if asset_ids_to_archive:
            self.log.debug("{}Archivation of assets <{}>".format(
                mongo_proc_txt,
                ", ".join([str(id) for id in asset_ids_to_archive])
            ))
            self.dbcon.update_many(
                {
                    "_id": {"$in": asset_ids_to_archive},
                    "type": "asset"
                },
                {"$set": {"type": "archived_asset"}}
            )

        if subset_ids_to_archive:
            self.log.debug("{}Archivation of subsets <{}>".format(
                mongo_proc_txt,
                ", ".join([str(id) for id in subset_ids_to_archive])
            ))
            self.dbcon.update_many(
                {
                    "_id": {"$in": subset_ids_to_archive},
                    "type": "subset"
                },
                {"$set": {"type": "archived_subset"}}
            )

        if ftrack_ids_to_delete:
            self.log.debug("{}Deleting Ftrack Entities <{}>".format(
                ftrack_proc_txt, ", ".join(ftrack_ids_to_delete)
            ))

            ftrack_ents_to_delete = (
                self._filter_entities_to_delete(ftrack_ids_to_delete, session)
            )
            for entity in ftrack_ents_to_delete:
                session.delete(entity)
                try:
                    session.commit()
                except Exception:
                    ent_path = "/".join(
                        [ent["name"] for ent in entity["link"]]
                    )
                    msg = "Failed to delete entity"
                    report_messages[msg].append(ent_path)
                    session.rollback()
                    self.log.warning(
                        "{} <{}>".format(msg, ent_path),
                        exc_info=True
                    )

        if not_deleted_entities_id:
            joined_not_deleted = ", ".join([
                "\"{}\"".format(ftrack_id)
                for ftrack_id in not_deleted_entities_id
            ])
            joined_asset_names = ", ".join([
                "\"{}\"".format(name)
                for name in asset_names_to_delete
            ])
            # Find assets of selected entities with names of checked subsets
            assets = session.query((
                "select id from Asset where"
                " context_id in ({}) and name in ({})"
            ).format(joined_not_deleted, joined_asset_names)).all()

            self.log.debug("{}Deleting Ftrack Assets <{}>".format(
                ftrack_proc_txt,
                ", ".join([asset["id"] for asset in assets])
            ))
            for asset in assets:
                session.delete(asset)
                try:
                    session.commit()
                except Exception:
                    session.rollback()
                    msg = "Failed to delete asset"
                    report_messages[msg].append(asset["id"])
                    self.log.warning(
                        "Asset: {} <{}>".format(asset["name"], asset["id"]),
                        exc_info=True
                    )

        return self.report_handle(report_messages, project_name, event)

    def _filter_entities_to_delete(self, ftrack_ids_to_delete, session):
        """Filter children entities to avoid CircularDependencyError."""
        joined_ids_to_delete = ", ".join(
            ["\"{}\"".format(id) for id in ftrack_ids_to_delete]
        )
        to_delete_entities = session.query(
            "select id, link from TypedContext where id in ({})".format(
                joined_ids_to_delete
            )
        ).all()
        filtered = to_delete_entities[:]
        while True:
            changed = False
            _filtered = filtered[:]
            for entity in filtered:
                entity_id = entity["id"]

                for _entity in tuple(_filtered):
                    if entity_id == _entity["id"]:
                        continue

                    for _link in _entity["link"]:
                        if entity_id == _link["id"] and _entity in _filtered:
                            _filtered.remove(_entity)
                            changed = True
                            break

            filtered = _filtered

            if not changed:
                break

        return filtered

    def report_handle(self, report_messages, project_name, event):
        if not report_messages:
            return {
                "success": True,
                "message": "Deletion was successful!"
            }

        title = "Delete report ({}):".format(project_name)
        items = []
        items.append({
            "type": "label",
            "value": "# Deleting was not completely successful"
        })
        items.append({
            "type": "label",
            "value": "<p><i>Check logs for more information</i></p>"
        })
        for msg, _items in report_messages.items():
            if not _items or not msg:
                continue

            items.append({
                "type": "label",
                "value": "# {}".format(msg)
            })

            if isinstance(_items, str):
                _items = [_items]
            items.append({
                "type": "label",
                "value": '<p>{}</p>'.format("<br>".join(_items))
            })
            items.append(self.splitter)

        self.show_interface(items, title, event)

        return {
            "success": False,
            "message": "Deleting finished. Read report messages."
        }


def register(session):
    '''Register plugin. Called when used as an plugin.'''

    DeleteAssetSubset(session).register()
