#!/usr/bin/env python3
"""Install the apps chosen in the installer that were not in the offline vault.

The installer writes only app IDs to /var/lib/windoesnot/pending-apps.json. This
user service resolves each ID against the signed catalogue (fixed Flatpak IDs,
repository packages or AUR names, never free text) and, once the network is up:
  * installs Flatpak apps per user from Flathub, without a password;
  * leaves repository and AUR apps for the Store (password / AUR review needed)
    and tells the user about them with one notification.
Finished apps are recorded in ~/.local/state/windoesnot/pending-apps-done.json."""
import argparse
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import time

BASE = Path(os.environ.get('WINDOESNOT_BASE', '/usr/share/windoesnot'))
PENDING = Path(os.environ.get('WINDOESNOT_PENDING', '/var/lib/windoesnot/pending-apps.json'))
STATE = Path(os.environ.get('XDG_STATE_HOME', Path.home() / '.local/state')) / 'windoesnot'
sys.path.insert(0, str(BASE / 'iso/software'))
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent / 'iso/software'))
from catalog import load_catalog, pending_reference, flatpak_remote  # noqa: E402

german = os.environ.get('LANG', '').lower().startswith('de')


def tr(de, en):
    return de if german else en


def read_json(path, default):
    try:
        return json.loads(Path(path).read_text())
    except (OSError, ValueError):
        return default


def remaining(catalog):
    wanted = read_json(PENDING, {}).get('pending', [])
    done = set(read_json(STATE / 'pending-apps-done.json', {}).get('done', []))
    known = {app['id']: app for app in catalog['apps']}
    return [known[i] for i in wanted if i in known and i not in done and known[i].get('source', 'offline') != 'offline']


def mark_done(app_id):
    STATE.mkdir(parents=True, exist_ok=True)
    path = STATE / 'pending-apps-done.json'
    done = set(read_json(path, {}).get('done', [])) | {app_id}
    path.write_text(json.dumps({'schema_version': 1, 'done': sorted(done)}, indent=2) + '\n')


def notify(title, body, dry):
    if dry or not shutil.which('notify-send'):
        print(f'NOTIFY: {title} | {body}')
        return
    subprocess.run(['notify-send', '-a', 'WinDoesNot', '-i', 'system-software-install', title, body], check=False)


def online():
    if shutil.which('nm-online'):
        return subprocess.run(['nm-online', '-q', '-t', '5']).returncode == 0
    return subprocess.run(['curl', '-fsI', '--max-time', '8', 'https://dl.flathub.org'], stdout=subprocess.DEVNULL).returncode == 0


def install_flatpak(app, dry, catalog):
    _, refs = pending_reference(app)
    remote, url = flatpak_remote(catalog, app)   # validated against the catalogue allowlist
    commands = [['flatpak', 'remote-add', '--user', '--if-not-exists', remote, url],
                ['flatpak', 'install', '--user', '--noninteractive', '-y', remote, refs[0]]]
    for command in commands:
        if dry:
            print('RUN:', ' '.join(command))
        elif subprocess.run(command, timeout=3600).returncode != 0:
            return False
    return True


FIREFOX_PREFS = """user_pref("toolkit.legacyUserProfileCustomizations.stylesheets", true);
user_pref("browser.shell.checkDefaultBrowser", false);
user_pref("browser.aboutwelcome.enabled", false);
user_pref("browser.startup.homepage_override.mstone", "ignore");
user_pref("browser.tabs.warnOnClose", false);
user_pref("browser.toolbars.bookmarks.visibility", "never");
user_pref("browser.sessionstore.resume_from_crash", false);
user_pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
user_pref("trailhead.firstrun.didSeeAboutWelcome", true);
"""
# No tab strip, address bar, menu or bookmarks bar: the page fills the window like an app.
FIREFOX_CHROME = """#TabsToolbar, #nav-bar, #toolbar-menubar, #PersonalToolbar, #sidebar-main, #sidebar-box { visibility: collapse !important; }
:root[tabsintitlebar] #titlebar { display: none !important; }
"""


def seed_firefox_profile(profile, dry):
    if dry:
        return
    (profile / 'chrome').mkdir(parents=True, exist_ok=True)
    (profile / 'user.js').write_text(FIREFOX_PREFS)
    (profile / 'chrome/userChrome.css').write_text(FIREFOX_CHROME)


def webapp_command(app_id, url, dry=False):
    """(Exec argv, window class). Own window class per web app so the taskbar groups it as its own app.

    Chromium family in app mode if present, else Firefox (always in the image) with a private
    profile whose toolbars are hidden; never the user's normal Firefox profile."""
    wmclass = f'windoesnot-webapp-{app_id}'
    profile = Path.home() / '.local/share/windoesnot/webapps' / app_id
    for binary in ('google-chrome-stable', 'chromium', 'brave'):
        if shutil.which(binary):
            return [binary, f'--app={url}', f'--user-data-dir={profile}', f'--class={wmclass}', f'--name={wmclass}'], wmclass
    if shutil.which('flatpak') and subprocess.run(['flatpak', 'info', 'com.google.Chrome'], stdout=subprocess.DEVNULL,
                                                  stderr=subprocess.DEVNULL).returncode == 0:
        return ['flatpak', 'run', 'com.google.Chrome', f'--app={url}', f'--class={wmclass}'], wmclass
    seed_firefox_profile(profile / 'firefox', dry)
    return ['firefox', '--no-remote', '--profile', str(profile / 'firefox'), '--name', wmclass, '--class', wmclass,
            '--new-window', url], wmclass


def webapp_icon(app_id, dry):
    """Copy the catalogue icon into the user's icon theme and return its absolute path (empty if unknown)."""
    for base in (BASE, Path(__file__).resolve().parent.parent.parent):
        source = base / 'iso/software/icons' / f'{app_id}.png'
        if source.is_file():
            target = Path.home() / '.local/share/icons/hicolor/64x64/apps' / f'windoesnot-webapp-{app_id}.png'
            if not dry:
                target.parent.mkdir(parents=True, exist_ok=True)
                shutil.copyfile(source, target)
            return str(target)
    return ''


def write_webapp(app, name, dry, link=False):
    """Per-user launcher; idempotent, rewritten when the available browser changes. Returns True if (re)written."""
    _, refs = pending_reference(app)
    wmclass = ''
    if link:
        command = ['xdg-open', refs[0]]
    else:
        command, wmclass = webapp_command(app['id'], refs[0], dry)
    quoted = ' '.join('"' + part.replace('\\', '\\\\').replace('"', '\\"') + '"' if any(c in part for c in ' "') else part for part in command)
    comment = app['description']['de' if german else 'en']
    icon = webapp_icon(app['id'], dry) or ('applications-games' if link else 'web-browser')
    tail = 'Categories=Game;\nX-WinDoesNot-Link=true\n' if link else f'Categories=Network;AudioVideo;\nStartupWMClass={wmclass}\nX-WinDoesNot-WebApp=true\n'
    text = ('[Desktop Entry]\nType=Application\nVersion=1.0\n'
            f'Name={name}\nComment={comment}\nExec={quoted}\nIcon={icon}\nTerminal=false\n') + tail
    path = Path.home() / '.local/share/applications' / f'windoesnot-{"link" if link else "webapp"}-{app["id"]}.desktop'
    if path.is_file() and path.read_text() == text:
        return False
    if dry:
        print('WRITE:', path, '|', quoted, '| Icon=' + icon)
        return True
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(text)
    return True


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--dry-run', action='store_true', help='print what would run, change nothing')
    parser.add_argument('--no-wait', action='store_true', help='do not wait for the network, try once')
    args = parser.parse_args()
    catalog = load_catalog(BASE / 'iso/software/catalog.json') if (BASE / 'iso/software/catalog.json').is_file() \
        else load_catalog(Path(__file__).resolve().parent.parent.parent / 'iso/software/catalog.json')
    todo = remaining(catalog)
    if not todo:
        return 0
    store_apps = [a for a in todo if a['source'] in ('repo', 'aur')]
    flatpaks = [a for a in todo if a['source'] == 'flatpak']
    webapps = [a for a in todo if a['source'] == 'webapp']
    links = [a for a in todo if a['source'] == 'link']
    name = lambda a: a['name']['de' if german else 'en']  # noqa: E731
    # Launchers need no network: create them first so they exist even while Flatpaks wait for the internet.
    created = [name(a) for a in webapps if write_webapp(a, name(a), args.dry_run)] + [name(a) for a in links if write_webapp(a, name(a), args.dry_run, link=True)]
    if created and not args.dry_run:
        notify(tr('Web-Apps angelegt', 'Web apps created'), ', '.join(created), False)
    while flatpaks:
        if not args.dry_run and not online():
            if args.no_wait:
                return 1
            time.sleep(60)
            continue
        if not args.dry_run and not shutil.which('flatpak'):
            notify(tr('Programme warten', 'Apps are waiting'), tr('Flatpak fehlt, bitte im Store installieren: ', 'Flatpak is missing, please use the Store: ') + ', '.join(map(name, flatpaks)), args.dry_run)
            break
        failed = []
        for app in flatpaks:
            if install_flatpak(app, args.dry_run, catalog):
                if not args.dry_run:
                    mark_done(app['id'])
                    notify(tr('Programm installiert', 'App installed'), name(app), False)
            else:
                failed.append(app)
        flatpaks = failed
        if failed and args.no_wait:
            return 1
        if failed:
            time.sleep(300)
    notified_path = STATE / 'pending-apps-notified.json'
    notified = set(read_json(notified_path, {}).get('notified', []))
    store_apps = [a for a in store_apps if a['id'] not in notified]   # tell the user once per app
    if store_apps:
        if not args.dry_run:
            STATE.mkdir(parents=True, exist_ok=True)
            notified_path.write_text(json.dumps({'schema_version': 1, 'notified': sorted(notified | {a['id'] for a in store_apps})}) + '\n')
        notify(tr('Programme warten auf dich', 'Apps are waiting for you'),
               ', '.join(map(name, store_apps)) + tr(': im Store installieren (Passwort bzw. Prüfung nötig).', ': install them in the Store (password or review needed).'), args.dry_run)
    return 0


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