# -*- coding: utf-8 -*-

# Copyright (C) 2012-2015 Avencall
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>

import errno
from io import TextIOWrapper
import json
import os
import logging
import subprocess
from enum import StrEnum
from typing import Any, Dict, Optional
from pydantic import validator, ValidationError, BaseModel
from xivo_dao.alchemy.netiface import Netiface
from xivo_dao.helpers.db_utils import session_scope

from xivo import http_json_server
from xivo.http_json_server import CMD_R, CMD_RW

logger = logging.getLogger(__name__)

class HaNodeType(StrEnum):
    DISABLED = 'disabled'
    MASTER = 'master'
    SLAVE = 'slave'

class HAConfig(BaseModel, extra="forbid"):
    node_type: HaNodeType = HaNodeType.DISABLED
    remote_address: str = ''
    local_address: str = ''
    automatic_failover: bool = False
    active: bool = True if node_type == HaNodeType.MASTER else False

    @validator("node_type", pre=True, always=True)
    def ensure_enum(cls, v):
        if v is None:
            return HaNodeType.DISABLED
        try:
            return HaNodeType(v)
        except ValueError as exc:
            raise ValueError(f"node_type must be one of {list(HaNodeType)}") from exc

class HAConfigManager(object):
    DEFAULT_HA_CONF_FILE = '/etc/xivo/ha.conf'
    DEFAULT_HA_CONFIG = HAConfig()
    CRONFILE_SLAVE = 'xivo-ha-slave'
    CRONFILE_MASTER = 'xivo-ha-master'

    def __init__(self, postgres_config_updater_factory, cronfile_installer,
                 ha_conf_file=DEFAULT_HA_CONF_FILE):
        self._postgres_config_updater_factory = postgres_config_updater_factory
        self._cronfile_installer = cronfile_installer
        self._ha_conf_file: str = ha_conf_file

    def get_ha_config(self, args, options) -> Dict[str,Any]:
        return self._read_ha_config().dict()

    def _read_ha_config(self) -> HAConfig:
        try:
            with open(self._ha_conf_file, encoding='utf-8') as fobj:
                return self._read_ha_config_from_fobj(fobj)
        except IOError as e:
            if e.errno == errno.ENOENT:
                return self.DEFAULT_HA_CONFIG
            else:
                raise

    def _read_ha_config_from_fobj(self, fobj: TextIOWrapper) -> HAConfig:
        try:
            config_dictionnary = json.load(fobj)
        except json.JSONDecodeError as e:
            logger.error("Invalid HA configuration file: %s, returning default", e)
            return self.DEFAULT_HA_CONFIG
        config_dictionnary.setdefault('node_type', 'disabled')
        config_dictionnary.setdefault('automatic_failover', True)
        config_dictionnary.setdefault('active', config_dictionnary.get("node_type") == 'master')

        return self._convert_ha_config(config_dictionnary)

    def _convert_ha_config(self, dict_ha_config: dict[str,Any]) -> HAConfig:
        try:
            return HAConfig(**dict_ha_config)
        except ValidationError as e:
            raise ValueError(f"Invalid HA configuration: {e}") from e

    def search_voip_address(self) -> Optional[str]:
        with session_scope() as session:
            return session.query(Netiface.address).filter(Netiface.networktype == "voip").first()[0]

    def _add_local_address_if_missing(self, ha_config: HAConfig) -> HAConfig:
        if ha_config.local_address == '' and ha_config.node_type != HaNodeType.DISABLED:
            try:
                voip_address = self.search_voip_address()
                if voip_address:
                    ha_config.local_address = voip_address
            except Exception as e:
                logger.error("Error querying voip address from database: %s", e)
        return ha_config

    def update_ha_config(self, args, options):
        ha_config = self._convert_ha_config(args)
        ha_config = self._add_local_address_if_missing(ha_config)
        self._write_ha_config(ha_config)
        self._update_postgres(ha_config)
        self._update_cronfiles(ha_config)
        self._manage_services(ha_config)

    def update_ha_state(self, args, options):
        active_value = args.get('active') if isinstance(args, dict) else args[0]
        ha_conf = self._read_ha_config()

        if ha_conf.node_type == HaNodeType.MASTER:
            return

        ha_conf.active = bool(active_value)
        logger.info("Updating resilience state, services are: %s", bool(active_value))

        self._update_postgres(ha_conf)
        self._write_ha_config(ha_conf)

        command_args = ['/usr/sbin/xivo-manage-slave-services', 'start' if ha_conf.active else 'stop']

        subprocess.check_call(command_args, close_fds=True)


    def _write_ha_config(self, ha_config: HAConfig):
        with open(self._ha_conf_file, 'w', encoding='utf-8') as fobj:
            self._write_ha_config_to_fobj(ha_config, fobj)

    def _write_ha_config_to_fobj(self, ha_config: HAConfig, fobj):
        fobj.write(json.dumps(ha_config.dict()))

    def _update_postgres(self, ha_config: HAConfig):
        postgres_updater = self._postgres_config_updater_factory(ha_config)
        postgres_updater.update_pg_hba_file()
        postgres_updater.reload_postgres()

    def _update_cronfiles(self, ha_config: HAConfig):
        node_type = ha_config.node_type
        remote_address = ha_config.remote_address
        self._cronfile_installer.remove_cronfile(self.CRONFILE_MASTER)
        self._cronfile_installer.remove_cronfile(self.CRONFILE_SLAVE)
        if node_type == 'master':
            self._add_master_cronfile(remote_address)
        elif node_type == 'slave' and ha_config.automatic_failover:
            self._add_slave_cronfile(remote_address)

    def _add_master_cronfile(self, remote_address: str):
        content = '0 * * * * root /usr/sbin/xivo-master-slave-db-replication %s >/dev/null\n' \
                  '0 * * * * root /usr/bin/xivo-sync >/dev/null\n' % remote_address
        self._cronfile_installer.add_cronfile(self.CRONFILE_MASTER, content)

    def _add_slave_cronfile(self, remote_address: str):
        content = '* * * * * root /usr/sbin/xivo-check-master-status %s >/dev/null\n' % remote_address
        self._cronfile_installer.add_cronfile(self.CRONFILE_SLAVE, content)

    def _manage_services(self, ha_config: HAConfig):
        if ha_config.node_type != HaNodeType.SLAVE:
            command_args = ['/usr/sbin/xivo-manage-slave-services', 'start']
            subprocess.check_call(command_args, close_fds=True)


class _PostgresConfigUpdater(object):
    DEFAULT_CONFIG_DIR = '/var/lib/postgresql/17/data'
    PG_HBA_FILE = 'pg_hba.conf'

    def __init__(self, ha_config, postgres_config_dir=DEFAULT_CONFIG_DIR):
        self._ha_config = ha_config
        self._pg_hba_file = os.path.join(postgres_config_dir, self.PG_HBA_FILE)

    def update_pg_hba_file(self):
        self._clear_host_line_in_pg_hba()
        if self._ha_config.node_type == HaNodeType.SLAVE and not self._ha_config.active:
            self._append_host_line_in_pg_hba()

    def _clear_host_line_in_pg_hba(self):
        command_args = ['sed', '-i', '/^host asterisk postgres/d', self._pg_hba_file]
        subprocess.check_call(command_args, close_fds=True)

    def _append_host_line_in_pg_hba(self):
        master_ip_address = self._ha_config.remote_address
        host_line = 'host asterisk postgres %s/32 trust\n' % master_ip_address
        with open(self._pg_hba_file, 'a', encoding='utf-8') as fobj:
            fobj.write(host_line)

    def reload_postgres(self):
        # Send a SIGHUP to postgres to make postgres re-read the pg_hba.conf.
        # See pg_hba.conf documentation
        command_args = ['/usr/bin/xivo-dcomp', 'kill', '-s', 'SIGHUP', 'db']
        subprocess.check_call(command_args, close_fds=True)


class _CronFileInstaller(object):
    DEFAULT_CRON_DIR = '/etc/cron.d'

    def __init__(self, cron_dir=DEFAULT_CRON_DIR):
        self._cron_dir = cron_dir

    def add_cronfile(self, filename, content):
        abs_filename = os.path.join(self._cron_dir, filename)
        with open(abs_filename, 'w', encoding='utf-8') as fobj:
            fobj.write(content)

    def remove_cronfile(self, filename):
        abs_filename = os.path.join(self._cron_dir, filename)
        try:
            os.unlink(abs_filename)
        except OSError as e:
            if e.errno != errno.ENOENT:
                raise


ha_config_manager = HAConfigManager(_PostgresConfigUpdater, _CronFileInstaller())
http_json_server.register(ha_config_manager.get_ha_config, CMD_R,
                          name='get_ha_config')
http_json_server.register(ha_config_manager.update_ha_config, CMD_RW,
                          name='update_ha_config')
http_json_server.register(ha_config_manager.update_ha_state, CMD_RW,
                          name='update_ha_state')
