#!/usr/local/venvs/pg_backup_service/bin/python

import argparse
import logging
import signal
import sys
import textwrap
import time
from math import floor, log
from typing import Any, Union

from packaging.version import Version
from pgcos import PgUtil


class PgBackupService:
    __slots__ = ["args", "debug", "dryrun", "logger", "start_time", "delay", "password", "pg", "version"]  # noqa: RUF023

    def __init__(self):
        self.args = self.get_args()
        self.debug = self.args.debug
        self.dryrun = self.args.debug
        self.logger = self.configure_logger()
        self.start_time = time.time()
        self.delay = 5
        self.pg = PgUtil(
            {
                "host": "/var/run/postgresql",
                "database": "postgres",
                "user": "postgres",
                "application_name": f"{self.__class__.__name__}",
            }
        )
        self.version = self.get_server_major_version()
        self.logger.info(f"{self.__class__.__name__} instance created, running against postgres {self.version}")

        signal.signal(signal.SIGTERM, self.__handle_signal)
        signal.signal(signal.SIGINT, self.__handle_signal)

    @staticmethod
    def format_time(seconds: Union[int, float]) -> str:
        """Return human readable string given seconds input."""
        if seconds < 1:
            power = 0
        else:
            power = floor(log(seconds, 60))
        return f"{round(seconds / 60 ** power, 2)} {['s', 'm', 'h'][int(power)]}"

    @staticmethod
    def get_args() -> argparse.Namespace:
        """Parse and return CLI arguments."""
        parser = argparse.ArgumentParser(
            description=textwrap.dedent(
                """
                Utility to start and stop postgres backup using the low level API.
                Intended to be run as a systemd service as postgres user.
                Cleanup is done on SIGTERM and SIGINT.
                https://www.postgresql.org/docs/current/continuous-archiving.html#BACKUP-LOWLEVEL-BASE-BACKUP
                https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADMIN-BACKUP
            """
            ),
            formatter_class=argparse.RawDescriptionHelpFormatter,
        )
        parser.add_argument("--debug", help="debug mode", action="store_true")
        parser.add_argument("--dryrun", help="show what would be done, but do nothing", action="store_true")
        return parser.parse_args()

    def configure_logger(self) -> logging.Logger:
        """Configure the logger."""
        logger = logging.getLogger("__name__")
        log_format = f"{'DRYRUN - ' if self.dryrun else ''}{'%(levelname)s - ' if self.debug else ''}%(message)s"
        logging.basicConfig(
            format=log_format,
            datefmt="%Y-%m-%d %H:%M:%S",
            level=logging.INFO,
            handlers=[logging.StreamHandler()],
            force=True,
        )
        if self.debug:
            logger.setLevel(logging.DEBUG)
        return logger

    def pg_connect(self) -> None:
        """Connect to postgresql database."""
        error_log = "could not connect to local postgres"
        try:
            self.pg.connect()
            if not self.pg.is_connected():
                self.logger.error(f"{error_log}")
                sys.exit(1)
        except Exception as err:
            self.logger.error(f"{error_log}: {err}")  # noqa: TRY400
            sys.exit(1)

    def get_server_major_version(self) -> Version:
        """Retrieve postgres server version."""
        statement = "SELECT CURRENT_SETTING('server_version')"
        self.pg_connect()
        raw_version = self.pg.execute_fetch_one(statement=statement)[0]
        return Version(raw_version.split(" ")[0])

    def __handle_signal(self, sig_num: int, curr_stack_frame: Any):
        """Handle signal received, to use exclusively in signal.signal()."""
        self.logger.info(f"{signal.strsignal(sig_num)} received")
        self.stop()

    def start(self) -> None:
        """Start postgres backup."""
        label = "pitr_snapshot"
        data = [label]

        if self.version >= Version("15"):
            statement = "SELECT PG_BACKUP_START(%s, true)"
        else:
            statement = "SELECT PG_START_BACKUP(%s, true, false)"

        self.pg_connect()

        if self.debug:
            self.logger.debug(f"{self.pg.mogrify(statement=statement, data=data)}")
        elif not self.debug and self.dryrun:
            self.logger.info(f"{self.pg.mogrify(statement=statement, data=data)}")

        while True:
            if not self.dryrun:
                try:
                    pg_backup_start = self.pg.execute_fetch_one(statement=statement, data=data)[0]
                    self.logger.info(f"backup started at WAL location '{pg_backup_start} with label '{label}'")
                except Exception as err:
                    if "WAL generated with full_page_writes=off was replayed" in str(err):
                        self.logger.info(
                            "starting a backup an a standby with full_page_writes=off on the primary is not possible"
                        )
                        sys.exit()
                    if "ERROR: a backup is already in progress" not in str(err):
                        self.logger.error(err)  # noqa: TRY400
                        sys.exit(1)
            time.sleep(self.delay)
            self.logger.info("backup in progress...")

    def stop(self) -> None:
        """Stop postgres backup gracefully."""
        self.logger.info("stopping backup")

        if self.version >= Version("15"):
            statement = "SELECT PG_BACKUP_STOP()"
        else:
            statement = "SELECT PG_STOP_BACKUP(false)"

        if self.debug:
            self.logger.debug(f"{self.pg.mogrify(statement=statement)}")
        elif not self.debug and self.dryrun:
            self.logger.info(f"{self.pg.mogrify(statement=statement)}")

        if not self.dryrun:
            try:
                pg_stop_backup = self.pg.execute_fetch_one(statement)[0]
            except Exception as err:
                self.logger.error(err)  # noqa: TRY400
                sys.exit(1)
            self.logger.info(f"backup finished:\n{pg_stop_backup}")

        end_time = time.time()
        self.logger.info(f"service ran for {self.format_time(end_time - self.start_time)}")
        sys.exit(0)


def main():
    service = PgBackupService()
    service.start()


if __name__ == "__main__":
    main()
