#!/usr/bin/python3
# SPDX-License-Identifier: LicenseRef-Proprietary
"""Replace only the former packaged notification command with the native service."""

from copy import deepcopy
import os
from pathlib import Path
import re
import stat
import sys
import tempfile
import tomllib

NATIVE = ['systemctl', '--user', 'start', '--no-block', 'gravitas-notifications.service']


def notifications_config(text):
    current = tomllib.loads(text)
    commands = current.get('spawn', [])
    if not isinstance(commands, list):
        return text
    for index, command in enumerate(commands):
        if command != ['mako']:
            continue
        expected = deepcopy(current)
        expected['spawn'][index] = NATIVE
        for match in re.finditer(r'''\[\s*(['"])mako\1\s*,?\s*\]''', text):
            changed = text[:match.start()] + '["systemctl", "--user", "start", "--no-block", "gravitas-notifications.service"]' + text[match.end():]
            try:
                valid = tomllib.loads(changed) == expected
            except tomllib.TOMLDecodeError:
                valid = False
            if valid:
                text, current = changed, expected
                break
        else:
            raise ValueError('Cannot safely replace the stock notification command; existing settings retained.')
    return text


def migrate(configured):
    if not configured.exists() and not configured.is_symlink():
        return False
    path = configured.resolve(strict=True)
    metadata = path.stat()
    if not stat.S_ISREG(metadata.st_mode) or metadata.st_uid != os.geteuid():
        raise ValueError('The compositor config must be a regular file owned by the login user.')
    original = path.read_bytes()
    changed = notifications_config(original.decode()).encode()
    backup = path.with_name(path.name + '.pre-native-notifications')
    if changed == original or backup.exists() or backup.is_symlink():
        return False

    def unchanged():
        latest = path.stat()
        if (latest.st_dev, latest.st_ino, latest.st_mtime_ns, latest.st_ctime_ns, latest.st_size) != (
                metadata.st_dev, metadata.st_ino, metadata.st_mtime_ns, metadata.st_ctime_ns, metadata.st_size) or path.read_bytes() != original:
            raise RuntimeError('Settings changed during notification migration; try again at the next login.')

    with tempfile.NamedTemporaryFile(mode='wb', dir=path.parent, delete=False) as stream:
        temporary = Path(stream.name)
        backup_identity = None
        committed = False
        try:
            stream.write(changed)
            stream.flush()
            os.fsync(stream.fileno())
            os.fchmod(stream.fileno(), stat.S_IMODE(metadata.st_mode))
            unchanged()
            descriptor = os.open(backup, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
            created = os.fstat(descriptor)
            backup_identity = (created.st_dev, created.st_ino)
            with os.fdopen(descriptor, 'wb') as saved:
                os.fchmod(saved.fileno(), stat.S_IMODE(metadata.st_mode))
                saved.write(original)
                saved.flush()
                os.fsync(saved.fileno())
            unchanged()
            os.replace(temporary, path)
            committed = True
            directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
            try:
                os.fsync(directory)
            finally:
                os.close(directory)
        finally:
            temporary.unlink(missing_ok=True)
            if backup_identity is not None and not committed:
                # A backup marks a completed migration, including an intentional revert.
                # Retain that opt-out only after replacement; a failed attempt must retry.
                try:
                    pending = backup.lstat()
                except FileNotFoundError:
                    pass
                else:
                    if (pending.st_dev, pending.st_ino) == backup_identity:
                        backup.unlink()
    return True


def main():
    path = Path(os.environ.get('XDG_CONFIG_HOME', Path.home() / '.config')) / 'gravitas/compositor.toml'
    try:
        if migrate(path):
            print('Gravitas: enabled native notifications; original saved with .pre-native-notifications.')
    except (OSError, ValueError, RuntimeError) as error:
        print(f'Gravitas notification migration: {error}', file=sys.stderr)


if __name__ == '__main__':
    main()
