#!/usr/bin/python3
# SPDX-License-Identifier: LicenseRef-Proprietary
"""Move exact packaged panel startup into its privileged compositor supervisor."""

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


STOCK = ['waybar', '-c', '/usr/share/gravitas/waybar.jsonc', '-s', '/usr/share/gravitas/waybar.css']
MANAGED = ['systemctl', '--user', 'start', '--no-block', 'gravitas-panel.service']
PANEL = ['/usr/bin/gravitas-panel']


def tokens(text):
    """Yield structural tokens outside TOML strings and comments."""
    i = 0
    while i < len(text):
        c = text[i]
        if c == '#':
            end = text.find('\n', i)
            i = len(text) if end < 0 else end + 1
        elif c in ('"', "'"):
            delimiter = c * (3 if text.startswith(c * 3, i) else 1)
            i += len(delimiter)
            while i < len(text):
                if c == '"' and text[i] == '\\':
                    i += 2
                elif text.startswith(delimiter, i):
                    i += len(delimiter)
                    break
                else:
                    i += 1
        elif c.isspace():
            i += 1
        else:
            yield i, c
            i += 1


def remove_command(text, command, expected):
    structure = list(tokens(text))
    starts = []
    for index, (end, token) in enumerate(structure):
        if token == '[':
            starts.append((end, index))
        elif token == ']' and starts:
            start, start_index = starts.pop()
            try:
                value = tomllib.loads('value = ' + text[start:end + 1])['value']
            except tomllib.TOMLDecodeError:
                continue
            if value != command:
                continue
            commas = [None]
            if index + 1 < len(structure) and structure[index + 1][1] == ',':
                commas.append(structure[index + 1][0])
            if start_index and structure[start_index - 1][1] == ',':
                commas.append(structure[start_index - 1][0])
            for comma in commas:
                edits = [(start, end + 1)] + ([(comma, comma + 1)] if comma is not None else [])
                changed = text
                for left, right in sorted(edits, reverse=True):
                    changed = changed[:left] + changed[right:]
                try:
                    if tomllib.loads(changed) == expected:
                        return changed
                except tomllib.TOMLDecodeError:
                    pass
    raise ValueError('Cannot safely remove the packaged taskbar command; existing startup retained.')


def panel_config(text):
    """Preserve personal argv, disabled startup and explicit security.panel choices."""
    current = tomllib.loads(text)
    commands = current.get('spawn', [])
    if not isinstance(commands, list):
        return text
    targets = [i for i, command in enumerate(commands) if command in (STOCK, MANAGED)]
    if not targets:
        return text
    for index in reversed(targets):
        expected = deepcopy(current)
        command = expected['spawn'].pop(index)
        text = remove_command(text, command, expected)
        current = expected
    if 'panel' in current.get('security', {}):
        return text
    expected = deepcopy(current)
    expected.setdefault('security', {})['panel'] = PANEL
    newline = '\r\n' if '\r\n' in text else '\n'
    setting = 'panel = ["/usr/bin/gravitas-panel"]' + newline
    if 'security' not in current:
        changed = text + ('' if text.endswith('\n') else newline) + newline + '[security]' + newline + setting
        if tomllib.loads(changed) == expected:
            return changed
    else:
        for match in re.finditer(r'(?m)^[ \t]*\[[ \t]*(?:security|"security"|\'security\')[ \t]*\][ \t]*(?:#[^\r\n]*)?\r?$', text):
            end = text.find('\n', match.end())
            end = len(text) if end < 0 else end + 1
            changed = text[:end] + ('' if end and text[end - 1] == '\n' else newline) + setting + text[end:]
            if tomllib.loads(changed) == expected:
                return changed
        starts = []
        for end, token in tokens(text):
            if token == '{':
                starts.append(end)
            elif token == '}' and starts:
                start = starts.pop()
                try:
                    value = tomllib.loads('value = ' + text[start:end + 1])['value']
                except tomllib.TOMLDecodeError:
                    continue
                if value != current['security']:
                    continue
                changed = text[:start + 1] + setting.strip() + (', ' if value else '') + text[start + 1:]
                if tomllib.loads(changed) == expected:
                    return changed
    raise ValueError('Cannot safely add the privileged panel setting; existing startup retained.')


def migrate(configured):
    """Atomically update a regular config, retaining its symlink, mode and first backup."""
    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 = panel_config(original.decode('utf-8')).encode('utf-8')
    if changed == original:
        return False
    backup = path.with_name(path.name + '.pre-panel')
    try:
        descriptor = os.open(backup, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
    except FileExistsError:
        pass
    else:
        with os.fdopen(descriptor, 'wb') as stream:
            os.fchmod(stream.fileno(), stat.S_IMODE(metadata.st_mode))
            stream.write(original)
            stream.flush()
            os.fsync(stream.fileno())
    with tempfile.NamedTemporaryFile(mode='wb', dir=path.parent, delete=False) as stream:
        temporary = Path(stream.name)
        try:
            stream.write(changed)
            stream.flush()
            os.fsync(stream.fileno())
            os.fchmod(stream.fileno(), stat.S_IMODE(metadata.st_mode))
            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 panel migration; try again at the next login.')
            os.replace(temporary, path)
        finally:
            temporary.unlink(missing_ok=True)
    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 supervised taskbar previews; original config saved with .pre-panel.')
    except (OSError, ValueError, RuntimeError) as error:
        # An optional default migration must not prevent the existing session from starting.
        print(f'Gravitas panel migration: {error}', file=sys.stderr)


if __name__ == '__main__':
    main()
