#!/usr/bin/env python3
"""Otto OneDrive-Sync — spiegelt einen lokalen Antrags-Workspace in den
OneDrive-Ordner Plappi/. Laeuft als User 'nk' (liest /home/nk Workspace),
holt den Graph-Token per `sudo -u agent _od_token.py`.

Nur echte Doku wird gesynct (.md/.docx/.xlsx/.pdf); Scratch (_*, *.png, *.py,
Dotfiles, __pycache__) wird uebersprungen. Aenderungs-Erkennung via sha256-
Manifest → nur geaenderte/neue Dateien werden hochgeladen (idempotent).

Usage:
  onedrive-sync.py                 # default: plappi-Workspace
  onedrive-sync.py <local_dir>     # anderer Workspace (in Plappi/ gespiegelt)
"""
import os, sys, json, hashlib, subprocess, urllib.request, urllib.parse

DRIVE = "b!XAhltkZhfUCTfxe1WwksbeUH1YxptIVGoT43oPMC_ECbiaw4ncFDTrhiHvflGezt"
PLAPPI_ROOT_ID = "01LU3Y225F6DGKDHF2EVA2462D2QZLCRUT"  # OneDrive Ordner "Plappi"
DEFAULT_LOCAL = "/home/nk/hobo-godmode/otto/projekte/plappi"
MANIFEST_NAME = ".onedrive-sync-manifest.json"
KEEP_EXT = ('.md', '.docx', '.xlsx', '.pdf', '.pptx', '.csv')

# Inline-Token-Holer: laeuft als 'agent' (liest MSAL-Cache 600 agent). Per -c uebergeben,
# damit agent keine Datei unter /home/nk lesen muss (Traversal-Sperre).
_TOKEN_CODE = r'''
import json,time,urllib.request,urllib.parse
c=json.load(open("/home/agent/etc/msal-token-cache.json"))
for v in c.get("AccessToken",{}).values():
    if int(v.get("expires_on",0))>time.time()+120:
        print(v["secret"]);raise SystemExit
rt=next(iter(c["RefreshToken"].values()))
t=rt["home_account_id"].split(".")[1]
s=next(iter(c["AccessToken"].values()))["target"]
d=urllib.parse.urlencode({"client_id":rt["client_id"],"grant_type":"refresh_token","refresh_token":rt["secret"],"scope":s}).encode()
r=urllib.request.Request(f"https://login.microsoftonline.com/{t}/oauth2/v2.0/token",data=d,method="POST",headers={"Content-Type":"application/x-www-form-urlencoded"})
print(json.loads(urllib.request.urlopen(r).read())["access_token"])
'''

def get_token():
    out = subprocess.run(
        ['sudo', '-u', 'agent', '/home/agent/venv/bin/python3', '-c', _TOKEN_CODE],
        capture_output=True, text=True)
    tok = out.stdout.strip()
    if not tok:
        sys.exit(f"Token-Fehler: {out.stderr.strip() or 'leer'}")
    return tok

class Graph:
    def __init__(self, tok): self.tok = tok
    def _req(self, method, url, data=None, ctype=None):
        h = {'Authorization': f'Bearer {self.tok}'}
        if ctype: h['Content-Type'] = ctype
        req = urllib.request.Request(url, data=data, method=method, headers=h)
        return json.loads(urllib.request.urlopen(req).read())
    def child_by_name(self, parent_id, name):
        url = (f"https://graph.microsoft.com/v1.0/drives/{DRIVE}/items/{parent_id}"
               f"/children?$select=id,name,folder&$top=400")
        for it in self._req('GET', url).get('value', []):
            if it['name'] == name:
                return it
        return None
    def ensure_folder(self, parent_id, name):
        ex = self.child_by_name(parent_id, name)
        if ex and ex.get('folder'):
            return ex['id']
        url = f"https://graph.microsoft.com/v1.0/drives/{DRIVE}/items/{parent_id}/children"
        body = json.dumps({"name": name, "folder": {},
                           "@microsoft.graph.conflictBehavior": "fail"}).encode()
        return self._req('POST', url, body, 'application/json')['id']
    def put_file(self, parent_id, local, name):
        with open(local, 'rb') as f:
            content = f.read()
        url = (f"https://graph.microsoft.com/v1.0/drives/{DRIVE}/items/{parent_id}"
               f":/{urllib.parse.quote(name)}:/content")
        return self._req('PUT', url, content, 'application/octet-stream').get('size')

def want(fn):
    if fn.startswith('_') or fn.startswith('.'):
        return False
    return fn.lower().endswith(KEEP_EXT)

def sha(path):
    h = hashlib.sha256()
    with open(path, 'rb') as f:
        for b in iter(lambda: f.read(65536), b''):
            h.update(b)
    return h.hexdigest()

def main():
    local = os.path.abspath(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_LOCAL
    if not os.path.isdir(local):
        sys.exit(f"kein Verzeichnis: {local}")
    g = Graph(get_token())
    mpath = os.path.join(local, MANIFEST_NAME)
    manifest = {}
    if os.path.exists(mpath):
        try: manifest = json.load(open(mpath))
        except Exception: manifest = {}
    folder_cache = {'': PLAPPI_ROOT_ID}

    def folder_id(reldir):
        if reldir in folder_cache:
            return folder_cache[reldir]
        parent = folder_id(os.path.dirname(reldir))
        fid = g.ensure_folder(parent, os.path.basename(reldir))
        folder_cache[reldir] = fid
        return fid

    import time as _t
    uploaded, skipped, failed = [], 0, []
    for root, dirs, files in os.walk(local):
        dirs[:] = [d for d in dirs if not d.startswith(('_', '.')) and d != '__pycache__']
        reldir = os.path.relpath(root, local)
        reldir = '' if reldir == '.' else reldir
        for fn in sorted(files):
            if not want(fn):
                continue
            lp = os.path.join(root, fn)
            relpath = fn if not reldir else f"{reldir}/{fn}"
            digest = sha(lp)
            if manifest.get(relpath) == digest:
                skipped += 1
                continue
            sz = None
            for attempt in range(3):
                try:
                    sz = g.put_file(folder_id(reldir), lp, fn)
                    break
                except urllib.error.HTTPError as e:
                    if e.code in (423, 409, 500, 502, 503, 504) and attempt < 2:
                        _t.sleep(2 * (attempt + 1)); continue
                    failed.append((relpath, f"HTTP {e.code}"))
                    break
                except Exception as e:
                    failed.append((relpath, str(e)[:60])); break
            if sz is not None:
                manifest[relpath] = digest
                uploaded.append((relpath, sz))

    json.dump(manifest, open(mpath, 'w'), indent=2)
    for rp, sz in uploaded:
        print(f"OK    Plappi/{rp}  ({sz}b)")
    for rp, why in failed:
        print(f"SKIP  Plappi/{rp}  ({why} — evtl. gerade offen/gesperrt)")
    print(f"\n=== Sync fertig: {len(uploaded)} hochgeladen, {skipped} unveraendert, {len(failed)} uebersprungen ===")
    sys.exit(1 if failed else 0)

if __name__ == '__main__':
    main()
