#!/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
##

# pylint: disable=E0213

import logging
from typing import NoReturn


from ops.framework import EventBase
from ops.main import main
from opslib.osm.charm import CharmedOsmBase
from opslib.osm.interfaces.zookeeper import ZookeeperCluster, ZookeeperServer
from opslib.osm.pod import ContainerV3Builder, PodSpecV3Builder
from opslib.osm.validator import ModelValidator, validator

logger = logging.getLogger(__name__)

CLIENT_PORT = 2181
SERVER_PORT = 2888
LEADER_ELECTION_PORT = 3888


class ConfigModel(ModelValidator):
    log_level: str
    image_pull_policy: str
    min_session_timeout: int
    max_session_timeout: int
    purge_interval: int
    snap_retain_count: int
    max_client_cnxns: int
    heap: int
    sync_limit: int
    init_limit: int
    tick_time: int

    @validator("log_level")
    def validate_log_level(cls, v):
        if v not in {"INFO", "DEBUG"}:
            raise ValueError("value must be INFO or DEBUG")
        return v

    @validator("image_pull_policy")
    def validate_image_pull_policy(cls, v):
        values = {
            "always": "Always",
            "ifnotpresent": "IfNotPresent",
            "never": "Never",
        }
        v = v.lower()
        if v not in values.keys():
            raise ValueError("value must be always, ifnotpresent or never")
        return values[v]


class ZookeeperCharm(CharmedOsmBase):
    """Zookeeper Charm."""

    def __init__(self, *args) -> NoReturn:
        """Zookeeper Charm constructor."""
        super().__init__(*args, oci_image="image")
        # Initialize Zookeeper cluster relation
        self.zookeeper_cluster = ZookeeperCluster(self, "cluster", CLIENT_PORT)
        self.framework.observe(self.on["cluster"].relation_changed, self._setup_cluster)
        # Initialize Zookeeper relation
        self.zookeeper_server = ZookeeperServer(self, "zookeeper")
        self.framework.observe(self.on["zookeeper"].relation_joined, self._publish_info)

    @property
    def num_units(self):
        return self.zookeeper_cluster.num_units

    @property
    def zookeeper_uri(self):
        return self.zookeeper_cluster.zookeeper_uri

    def _setup_cluster(self, event: EventBase):
        """Publishes Zookeeper information and reconfigures the pod.

        Args:
            event (EventBase): Zookeeper Cluster relation event.
        """
        self._publish_zookeeper_info(event)
        self.configure_pod()

    def _publish_info(self, event: EventBase):
        """Publishes Zookeeper information.

        Args:
            event (EventBase): Zookeeper relation event.
        """
        if self.unit.is_leader():
            zk_uri = self.zookeeper_uri
            if zk_uri:
                self.zookeeper_server.publish_info(zk_uri)
            else:
                event.defer()

    def build_pod_spec(self, image_info):
        # Validate config
        config = ConfigModel(**dict(self.config))

        # Create Builder for the PodSpec
        pod_spec_builder = PodSpecV3Builder()

        # Build Container
        container_builder = ContainerV3Builder(
            self.app.name, image_info, config.image_pull_policy
        )

        container_builder.add_port(name="client", port=CLIENT_PORT)
        container_builder.add_port(name="server", port=SERVER_PORT)
        container_builder.add_port(name="leader-election", port=LEADER_ELECTION_PORT)
        container_builder.add_tcpsocket_readiness_probe(
            CLIENT_PORT,
            initial_delay_seconds=10,
            timeout_seconds=5,
            failure_threshold=6,
            success_threshold=1,
        )
        container_builder.add_tcpsocket_liveness_probe(
            CLIENT_PORT, initial_delay_seconds=20
        )
        container_builder.add_command(
            [
                "sh",
                "-c",
                " ".join(
                    [
                        "start-zookeeper",
                        f"--servers={self.num_units}",
                        "--data_dir=/var/lib/zookeeper/data",
                        "--data_log_dir=/var/lib/zookeeper/data/log",
                        "--conf_dir=/opt/zookeeper/conf",
                        f"--client_port={CLIENT_PORT}",
                        f"--election_port={LEADER_ELECTION_PORT}",
                        f"--server_port={SERVER_PORT}",
                        f"--tick_time={config.tick_time}",
                        f"--init_limit={config.init_limit}",
                        f"--sync_limit={config.sync_limit}",
                        f"--heap={config.heap}M",
                        f"--max_client_cnxns={config.max_client_cnxns}",
                        f"--snap_retain_count={config.snap_retain_count}",
                        f"--purge_interval={config.purge_interval}",
                        f"--max_session_timeout={config.max_session_timeout}",
                        f"--min_session_timeout={config.min_session_timeout}",
                        f"--log_level={config.log_level}",
                    ]
                ),
            ]
        )

        container = container_builder.build()

        # Add container to pod spec
        pod_spec_builder.add_container(container)

        return pod_spec_builder.build()


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