#!/usr/bin/env python3
"""SkyZ Files: Anmeldung mit einem normalen NFC-Tag.

sudo python3 SkyZFiles-NFC-Tag-20261012.py
Anschließend: Einstellungen → NFC-Tags → Tag-Link als URL auf den Tag schreiben.
"""
import ast
import base64
from datetime import datetime, timezone
import fcntl
import json
import os
from pathlib import Path
import pwd
import re
import sqlite3
import stat
import subprocess
import sys
import tempfile
import time
import urllib.error
import urllib.request

ROOT = Path('/data/skyzfiles')
SERVICES = ['skyzfiles-web', 'skyzfiles-worker']
PAYLOAD = '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'
import zlib

SETTINGS = """# BEGIN SKYZ NFC V1
if 'skyz_nfc.apps.NfcConfig' not in INSTALLED_APPS:
    INSTALLED_APPS = [*INSTALLED_APPS, 'skyz_nfc.apps.NfcConfig']
# END SKYZ NFC V1
"""
URLS = """# BEGIN SKYZ NFC V1
from skyz_nfc.urls import urlpatterns as skyz_nfc_urls
urlpatterns = skyz_nfc_urls + urlpatterns
# END SKYZ NFC V1
"""


def with_block(text, block):
    start, end = '# BEGIN SKYZ NFC V1', '# END SKYZ NFC V1'
    if text.count(start) != text.count(end) or text.count(start) > 1:
        raise RuntimeError('Unvollständiger NFC-Block; Datei zuerst prüfen.')
    if start in text:
        left = text.index(start)
        right = text.index(end, left) + len(end)
        text = text[:left] + text[right:]
    result = text.rstrip() + '\n\n' + block
    ast.parse(result)
    return result


def patch_files(app, assets):
    changes = dict(assets)
    for name, block in (('config/settings.py', SETTINGS), ('config/urls.py', URLS)):
        changes[name] = with_block((app / name).read_text(), block)
    name = 'templates/app.html'
    text = (app / name).read_text()
    if 'id="skyz-nfc-settings-link"' not in text:
        anchor = '<div class="settings-links">'
        if text.count(anchor) != 1:
            raise RuntimeError('Der Einstellungen-Bereich hat einen unbekannten Aufbau; keine Dateien geändert.')
        link = '<a id="skyz-nfc-settings-link" class="secondary" href="/account/nfc/">NFC-Tags</a>'
        text = text.replace(anchor, anchor + link, 1)
    changes[name] = text
    for name, text in changes.items():
        if Path(name).is_absolute() or '..' in Path(name).parts:
            raise RuntimeError('Ungültiger Update-Pfad.')
        if name.endswith('.py'):
            ast.parse(text)
    return changes


def run(args, **kwargs):
    return subprocess.run(args, check=True, timeout=120, **kwargs)


def regular(path):
    info = path.lstat()
    if not stat.S_ISREG(info.st_mode):
        raise RuntimeError('Keine reguläre Datei: ' + str(path))
    return info


def safe_parents(path, boundary):
    relative = path.relative_to(boundary)
    parent = boundary
    for part in relative.parts[:-1]:
        parent = parent / part
        if parent.is_symlink() or (parent.exists() and not parent.is_dir()):
            raise RuntimeError('Unerwarteter Update-Ordner: ' + str(parent))
    if path.is_symlink():
        raise RuntimeError('Unerwarteter Symlink: ' + str(path))


def write(path, content, uid=0, gid=0, mode=0o644):
    path.parent.mkdir(mode=0o755, parents=True, exist_ok=True)
    fd, temporary = tempfile.mkstemp(prefix='.skyz-nfc-', dir=path.parent)
    try:
        with os.fdopen(fd, 'wb') as handle:
            handle.write(content)
            handle.flush()
            os.fchown(handle.fileno(), uid, gid)
            os.fchmod(handle.fileno(), mode)
            os.fsync(handle.fileno())
        os.replace(temporary, path)
    finally:
        if os.path.exists(temporary):
            os.unlink(temporary)


def manage(*args, check=True):
    env = os.environ.copy()
    env.update(SKYZ_DATA_DIR=str(ROOT), SKYZ_ENV_FILE=str(ROOT / 'config/app.env'),
               PYTHONDONTWRITEBYTECODE='1')
    command = ['runuser', '-u', 'skyzfiles', '--', str(ROOT / 'venv/bin/python'),
               str(ROOT / 'app/manage.py'), *args]
    return subprocess.run(command, check=check, cwd=ROOT / 'app', env=env, timeout=120)


PREFLIGHT = """from config.wsgi import application
from django.conf import settings
from django.db import connection
import os, tempfile
assert settings.SESSION_ENGINE in ('django.contrib.sessions.backends.db', 'django.contrib.sessions.backends.cached_db'), 'Serverseitige Sitzungen erforderlich'
assert 'django_otp.middleware.OTPMiddleware' in settings.MIDDLEWARE
assert 'vault.middleware.AccessMiddleware' in settings.MIDDLEWARE
assert not settings.DEBUG and settings.SECRET_KEY
with connection.cursor() as cursor:
    cursor.execute('SELECT 1 FROM django_session LIMIT 1')
for name in ('state', 'static'):
    fd, name = tempfile.mkstemp(prefix='.skyz-nfc-check-', dir=settings.DATA_DIR / name)
    os.close(fd); os.unlink(name)
print('OK: vorhandene WebApp, Sitzungen und Schreibrechte.')
"""

VERIFY = """from config.wsgi import application
from django.conf import settings
from django.contrib.sessions.models import Session
from django.test import Client
from django.urls import resolve, reverse
from django.db.migrations.executor import MigrationExecutor
from django.db import connection
from skyz_nfc.models import NFCTagDevice
assert resolve('/account/login/').func.view_class.__module__ == 'skyz_nfc.views'
assert reverse('two_factor:login') == '/account/login/'
assert not MigrationExecutor(connection).migration_plan([('skyz_nfc', '0001_initial')])
NFCTagDevice.objects.exists()
client = Client(enforce_csrf_checks=True)
host = 'files.skyz.pet' if 'files.skyz.pet' in settings.ALLOWED_HOSTS else settings.ALLOWED_HOSTS[0]
try:
    for path in ('/account/login/', '/account/login/nfc/', '/account/nfc/scan/'):
        response = client.get(path, secure=True, HTTP_HOST=host)
        assert response.status_code == 200, (path, response.status_code)
    assert client.get('/account/nfc/', secure=True, HTTP_HOST=host).status_code == 302
    response = client.post('/account/nfc/confirm/', data='{}', content_type='application/json', secure=True, HTTP_HOST=host, HTTP_ORIGIN='https://' + host)
    assert response.status_code == 403, 'CSRF-Prüfung fehlt'
    assert client.get('/api/documents', secure=True, HTTP_HOST=host).status_code == 401
finally:
    if settings.SESSION_COOKIE_NAME in client.cookies:
        key = client.cookies[settings.SESSION_COOKIE_NAME].value
        Session.objects.filter(session_key=key).delete()
print('OK: Anmeldeseiten, NFC-Migration, CSRF und Zugriffsschutz.')
"""


def state(service):
    return subprocess.check_output(['systemctl', 'show', service, '-p', 'ActiveState', '--value'], text=True, timeout=20).strip()


def wait_ready():
    class NoRedirect(urllib.request.HTTPRedirectHandler):
        def redirect_request(self, *args):
            return None
    opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), NoRedirect())
    deadline = time.monotonic() + 20
    while time.monotonic() < deadline:
        req = urllib.request.Request('http://127.0.0.1:8097/account/login/',
                    headers={'Host': 'files.skyz.pet', 'X-Forwarded-Proto': 'https'})
        try:
            with opener.open(req, timeout=2) as response:
                if response.status == 200 and state('skyzfiles-web') == 'active':
                    return
        except (urllib.error.URLError, OSError):
            pass
        time.sleep(0.5)
    raise RuntimeError('Der Webdienst antwortet nach dem Neustart nicht.')


def snapshot_database(source, target):
    # Use SQLite's backup API so existing WAL content is included.
    with sqlite3.connect(source) as src, sqlite3.connect(target) as dest:
        src.backup(dest)
    os.chmod(target, 0o600)


def install():
    if os.geteuid() != 0:
        raise RuntimeError('Bitte mit sudo python3 ausführen.')
    pwd.getpwnam('skyzfiles')
    os.umask(0o022)
    app = ROOT / 'app'
    db = ROOT / 'state/db.sqlite3'
    for directory in (ROOT, app, app / 'config', ROOT / 'config', ROOT / 'state', ROOT / 'static'):
        if directory.is_symlink() or not directory.is_dir():
            raise RuntimeError('Unerwarteter Anwendungsordner: ' + str(directory))
    regular(db)
    regular(app / 'manage.py')
    for service in SERVICES:
        user = subprocess.check_output(['systemctl', 'show', service, '-p', 'User', '--value'], text=True, timeout=20).strip()
        if user != 'skyzfiles':
            raise RuntimeError('Abweichender Dienstbenutzer in ' + service)
    package = app / 'skyz_nfc'
    if package.exists() and ((package / 'VERSION').is_symlink() or not (package / 'VERSION').is_file()
                              or (package / 'VERSION').read_text().strip() != 'SkyZ Files NFC v1'):
        raise RuntimeError('Ein fremdes NFC-Modul ist bereits vorhanden; keine automatische Überschreibung.')
    assets = json.loads(zlib.decompress(base64.b64decode(PAYLOAD)))
    changes = patch_files(app, assets)
    originals = {}
    new_directories = set()
    for name in changes:
        path = app / name
        safe_parents(path, app)
        if path.exists():
            info = regular(path)
            originals[name] = {'bytes': path.read_bytes(), 'uid': info.st_uid, 'gid': info.st_gid,
                               'mode': stat.S_IMODE(info.st_mode)}
        else:
            originals[name] = None
        parent = path.parent
        while parent != app and not parent.exists():
            new_directories.add(parent)
            parent = parent.parent
    print('Prüfe die vorhandene Installation als skyzfiles ...', flush=True)
    try:
        manage('shell', '-c', PREFLIGHT)
    except subprocess.CalledProcessError as exc:
        raise RuntimeError('Die vorhandene WebApp oder ihre Rechte sind fehlerhaft. '
                           'Bei PermissionError zuerst SkyZFiles-Rechte-Reparatur-20261011.py ausführen.') from exc
    backup = ROOT / 'config' / ('nfc-backup-' + datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S-%f'))
    backup.mkdir(mode=0o700)
    metadata = {}
    for name, item in originals.items():
        metadata[name] = {k: v for k, v in item.items() if k != 'bytes'} if item else None
        if item:
            write(backup / name, item['bytes'], mode=0o600)
    write(backup / 'metadata.json', json.dumps(metadata, indent=2).encode(), mode=0o600)
    manifest = ROOT / 'static/staticfiles.json'
    old_manifest = None
    if manifest.exists():
        info = regular(manifest)
        old_manifest = (manifest.read_bytes(), info.st_uid, info.st_gid, stat.S_IMODE(info.st_mode))
        write(backup / 'staticfiles.json', old_manifest[0], mode=0o600)
    initial = {service: state(service) for service in SERVICES}
    restart = [s for s in SERVICES if initial[s] in ('active', 'activating', 'reloading')]
    if 'skyzfiles-web' not in restart:
        restart.insert(0, 'skyzfiles-web')
    stopped = changed = False
    try:
        print('Web- und Worker-Dienst kurz stoppen; Datenbank und Code sichern ...', flush=True)
        run(['systemctl', 'stop', *SERVICES])
        stopped = True
        snapshot_database(db, backup / 'db.sqlite3')
        for name, original in originals.items():
            path = app / name
            safe_parents(path, app)
            if original is not None and path.read_bytes() != original['bytes']:
                raise RuntimeError('Datei zwischenzeitlich geändert: ' + name)
            if original is None and path.exists():
                raise RuntimeError('Datei zwischenzeitlich angelegt: ' + name)
        changed = True
        for name, text in changes.items():
            write(app / name, text.encode('utf-8'))
        # Only this add-on's migration runs; existing vault migrations stay untouched.
        manage('migrate', 'skyz_nfc', '--noinput')
        manage('collectstatic', '--noinput')
        manage('check')
        manage('shell', '-c', VERIFY)
        run(['systemctl', 'reset-failed', *restart])
        run(['systemctl', 'start', *restart])
        wait_ready()
    except BaseException:
        if stopped:
            subprocess.run(['systemctl', 'stop', *SERVICES], check=False, timeout=30)
        if changed:
            for name, item in originals.items():
                path = app / name
                if item is None:
                    path.unlink(missing_ok=True)
                else:
                    write(path, item['bytes'], item['uid'], item['gid'], item['mode'])
            if old_manifest:
                write(manifest, *old_manifest)
            for directory in sorted(new_directories, key=lambda p: len(p.parts), reverse=True):
                try:
                    directory.rmdir()
                except OSError:
                    pass
        # Do not overwrite a live database after a start attempt: that could
        # discard unrelated user writes. Added NFC tables can remain unused;
        # Django recognizes their migration on the next installation attempt.
        original_running = [s for s in SERVICES if initial[s] in ('active', 'activating', 'reloading')]
        if stopped and original_running:
            subprocess.run(['systemctl', 'reset-failed', *original_running], check=False, timeout=30)
            subprocess.run(['systemctl', 'start', *original_running], check=False, timeout=30)
        print('Update fehlgeschlagen. Vorheriger Code wiederhergestellt; vorhandene Dokumentdaten bleiben erhalten.', file=sys.stderr)
        print('Sicherung: ' + str(backup), file=sys.stderr)
        raise
    print('\nNFC-Tag-Anmeldung installiert. Webdienst und Anmeldeseite antworten lokal.')
    print('Einrichten: https://files.skyz.pet/account/nfc/ oder Einstellungen → NFC-Tags.')
    print('Den erzeugten Link vollständig als URL auf deinen normalen NFC-Tag schreiben.')
    print('Passwort → Mit NFC-Tag bestätigen → Tag scannen; alternativ Tag zuerst, dann Passwort.')
    print('Keine zusätzliche Code- oder Face-ID-Abfrage. Authenticator und Ersatzcodes bleiben nutzbar.')
    print('Anleitung: /data/skyzfiles/app/NFC-TAGS.md')
    print('Sicherung: ' + str(backup))


def main():
    if len(sys.argv) > 1:
        raise RuntimeError('Aufruf: sudo python3 ' + Path(__file__).name)
    if os.geteuid() != 0:
        raise RuntimeError('Bitte mit sudo python3 ausführen.')
    for path in (ROOT, ROOT / 'config'):
        if path.is_symlink() or not path.is_dir():
            raise RuntimeError('Vorhandener Anwendungsordner fehlt: ' + str(path))
    # Serialize installer runs without keeping a secret in the lock file.
    lock = ROOT / 'config/.nfc-installer.lock'
    fd = os.open(lock, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
    try:
        try:
            fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
        except BlockingIOError as exc:
            raise RuntimeError('Ein NFC-Update läuft bereits.') from exc
        install()
    finally:
        os.close(fd)


if __name__ == '__main__':
    try:
        main()
    except (Exception, KeyboardInterrupt) as exc:
        print('FEHLER: ' + (str(exc) or 'Abgebrochen'), file=sys.stderr)
        sys.exit(1)
