#!/usr/bin/python3

# sd-debian: Software-defined Debian
# Copyright (C) 2024-2026 Patrik Dufresne <patrik@ikus-soft.com>
#
# SPDX-License-Identifier: GPL-3.0+
#
# 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 <https://www.gnu.org/licenses/>.

import getpass
import logging
import os
import shutil
import sys
from pathlib import Path

DPKG_STATUS = Path("/var/lib/dpkg/status")
CACHEDIR = Path("/var/cache/sd-debian")
SNAPSHOT_DIR = CACHEDIR / "dpkg-snapshot"
SNAPSHOT_FILE = SNAPSHOT_DIR / "status"
LOGDIR = Path("/var/log/sd-debian")
LOGFILE = LOGDIR / "sd-debian.log"

try:
    LOGUSER = getpass.getuser()
except (OSError, KeyError):
    LOGUSER = f"uid={os.getuid()}"

logger = logging.getLogger("dpkg_pre-invoke")


def setup_logger(path: Path):
    """
    Configure logging system.
    """
    try:
        path.parent.mkdir(parents=True, exist_ok=True)
    except OSError as e:
        print(
            f"[WARNING]: could not create log directory '{path.parent}': {e}, aborting\n",
            file=sys.stderr,
        )
        return

    writable = (path.exists() and os.access(path, os.W_OK)) or (
        not path.exists() and os.access(path.parent, os.W_OK)
    )
    if not writable:
        print(
            f"[WARNING]: log file at '{path}' is not writeable and we cannot create it, aborting\n",
            file=sys.stderr,
        )
        return

    if path.is_dir():
        print(
            f"[WARNING]: log path can not be a directory '{path}', aborting",
            file=sys.stderr,
        )
        return

    logging.basicConfig(
        filename=path,
        level=logging.DEBUG,
        format=f"%(asctime)s p=%(process)d u={LOGUSER} n=%(name)s %(levelname)s| %(message)s",
    )


def main() -> int:
    setup_logger(path=LOGFILE)

    if not DPKG_STATUS.is_file():
        logger.warning(f"'{DPKG_STATUS}' not found, cannot take snapshot")
        return 0

    tmp_file = SNAPSHOT_FILE.with_name(f".status.{os.getpid()}")
    try:
        SNAPSHOT_DIR.mkdir(parents=True, exist_ok=True)
        # Copy to a temp name, then rename over the destination: readers never
        # see a partial file, and any previous snapshot is replaced atomically.
        shutil.copyfile(DPKG_STATUS, tmp_file)
        os.chmod(tmp_file, 0o644)
        os.replace(tmp_file, SNAPSHOT_FILE)
        action = os.environ.get("DPKG_HOOK_ACTION", "unknown")
        logger.debug(f"copied '{DPKG_STATUS}' to '{SNAPSHOT_FILE}' (action={action})")
    except OSError as e:
        logger.error(f"failed to copy '{DPKG_STATUS}' to '{SNAPSHOT_FILE}': {e}")
        return 1
    finally:
        # Only present if the copy or rename failed part way.
        try:
            tmp_file.unlink()
        except OSError:
            pass

    return 0


if __name__ == "__main__":
    sys.exit(main())
