#!/usr/bin/env python3
# Copyright 2021 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# For those usages not covered by the Apache License, Version 2.0 please
# contact: legal@canonical.com
#
# To get in touch with the maintainers, please contact:
# osm-charmers@lists.launchpad.net
##

import logging
from typing import Dict, List, NoReturn

from ops.charm import CharmBase
from ops.framework import StoredState
from ops.main import main
from ops.model import ActiveStatus, Application, BlockedStatus, MaintenanceStatus, Unit
from oci_image import OCIImageResource, OCIImageResourceError

from pod_spec import make_pod_spec

logger = logging.getLogger(__name__)

GRAFANA_PORT = 3000


class RelationsMissing(Exception):
    def __init__(self, missing_relations: List):
        self.message = ""
        if missing_relations and isinstance(missing_relations, list):
            self.message += f'Waiting for {", ".join(missing_relations)} relation'
            if "," in self.message:
                self.message += "s"


class RelationDefinition:
    def __init__(self, relation_name: str, keys: List, source_type):
        if source_type != Application and source_type != Unit:
            raise TypeError(
                "source_type should be ops.model.Application or ops.model.Unit"
            )
        self.relation_name = relation_name
        self.keys = keys
        self.source_type = source_type


def check_missing_relation_data(
    data: Dict,
    expected_relations_data: List[RelationDefinition],
):
    missing_relations = []
    for relation_data in expected_relations_data:
        if not all(
            f"{relation_data.relation_name}_{k}" in data for k in relation_data.keys
        ):
            missing_relations.append(relation_data.relation_name)
    if missing_relations:
        raise RelationsMissing(missing_relations)


def get_relation_data(
    charm: CharmBase,
    relation_data: RelationDefinition,
) -> Dict:
    data = {}
    relation = charm.model.get_relation(relation_data.relation_name)
    if relation:
        self_app_unit = (
            charm.app if relation_data.source_type == Application else charm.unit
        )
        expected_type = relation_data.source_type
        for app_unit in relation.data:
            if app_unit != self_app_unit and isinstance(app_unit, expected_type):
                if all(k in relation.data[app_unit] for k in relation_data.keys):
                    for k in relation_data.keys:
                        data[f"{relation_data.relation_name}_{k}"] = relation.data[
                            app_unit
                        ].get(k)
                    break
    return data


class GrafanaCharm(CharmBase):
    """Grafana Charm."""

    state = StoredState()

    def __init__(self, *args) -> NoReturn:
        """Grafana Charm constructor."""
        super().__init__(*args)

        # Internal state initialization
        self.state.set_default(pod_spec=None)

        self.port = GRAFANA_PORT
        self.image = OCIImageResource(self, "image")

        # Registering regular events
        self.framework.observe(self.on.start, self.configure_pod)
        self.framework.observe(self.on.config_changed, self.configure_pod)

        # Registering required relation events
        self.framework.observe(self.on.prometheus_relation_changed, self.configure_pod)

        # Registering required relation broken events
        self.framework.observe(self.on.prometheus_relation_broken, self.configure_pod)

    @property
    def relations_requirements(self):
        return [RelationDefinition("prometheus", ["host", "port"], Unit)]

    def get_relation_state(self):
        relation_state = {}
        for relation_requirements in self.relations_requirements:
            data = get_relation_data(self, relation_requirements)
            relation_state = {**relation_state, **data}
        check_missing_relation_data(relation_state, self.relations_requirements)
        return relation_state

    def configure_pod(self, _=None) -> NoReturn:
        """Assemble the pod spec and apply it, if possible.

        Args:
            event (EventBase): Hook or Relation event that started the
                               function.
        """
        if not self.unit.is_leader():
            self.unit.status = ActiveStatus("ready")
            return

        relation_state = None
        try:
            relation_state = self.get_relation_state()
        except RelationsMissing as exc:
            logger.exception("Relation missing error")
            self.unit.status = BlockedStatus(exc.message)
            return

        self.unit.status = MaintenanceStatus("Assembling pod spec")

        # Fetch image information
        try:
            self.unit.status = MaintenanceStatus("Fetching image information")
            image_info = self.image.fetch()
        except OCIImageResourceError:
            self.unit.status = BlockedStatus("Error fetching image information")
            return

        try:
            pod_spec = make_pod_spec(
                image_info,
                self.model.config,
                relation_state,
                self.model.app.name,
                self.port,
            )
        except ValueError as exc:
            logger.exception("Config/Relation data validation error")
            self.unit.status = BlockedStatus(str(exc))
            return

        if self.state.pod_spec != pod_spec:
            self.model.pod.set_spec(pod_spec)
            self.state.pod_spec = pod_spec

        self.unit.status = ActiveStatus("ready")


if __name__ == "__main__":
    main(GrafanaCharm)