#!/usr/bin/env python3
from __future__ import annotations
import argparse, hashlib, json, os, shutil, subprocess, tempfile
from pathlib import Path
from zipfile import ZipFile

def sha256(path: Path) -> str:
    h=hashlib.sha256()
    with path.open('rb') as f:
        for c in iter(lambda:f.read(1024*1024),b''): h.update(c)
    return h.hexdigest()

def run(cmd, cwd, env=None):
    print('+',' '.join(map(str,cmd)))
    return subprocess.run(list(map(str,cmd)),cwd=cwd,env=env,check=True,text=True)

def safe_extract(zf: ZipFile, dest: Path):
    base=dest.resolve()
    for i in zf.infolist():
        n=i.filename.replace('\\','/')
        t=(dest/n).resolve()
        if n.startswith('/') or (base not in t.parents and t != base):
            raise RuntimeError(f'unsafe zip member: {i.filename}')
    zf.extractall(dest)

def content_root(extract: Path) -> Path:
    entries=[p for p in extract.iterdir() if p.name!='__MACOSX']
    if len(entries)==1 and entries[0].is_dir(): return entries[0]
    return extract

def clear_worktree(repo: Path):
    for p in repo.iterdir():
        if p.name=='.git': continue
        if p.is_dir() and not p.is_symlink(): shutil.rmtree(p)
        else: p.unlink()

def copy_tree(src: Path, dst: Path):
    for p in src.iterdir():
        target=dst/p.name
        if p.is_symlink(): raise RuntimeError(f'symlink not allowed in PFES import: {p}')
        if p.is_dir(): shutil.copytree(p,target)
        else: shutil.copy2(p,target)

def main():
    ap=argparse.ArgumentParser()
    ap.add_argument('--register',required=True,type=Path)
    ap.add_argument('--snapshot-dir',required=True,type=Path)
    ap.add_argument('--repo',required=True,type=Path)
    ap.add_argument('--apply',action='store_true')
    args=ap.parse_args()
    data=json.loads(args.register.read_text(encoding='utf-8'))
    for e in data['entries']:
        p=args.snapshot_dir/e['filename']
        if not p.is_file(): raise SystemExit(f'missing: {p}')
        got=sha256(p)
        if got.lower()!=e['sha256'].lower(): raise SystemExit(f'hash mismatch: {p}\nexpected {e["sha256"]}\ngot      {got}')
        print(f'OK {e["sequence"]:02d} {e["revision"]} {p.name} {got}')
    if not args.apply:
        print('dry-run validation passed; no repository changes made')
        return
    args.repo.mkdir(parents=True,exist_ok=True)
    if not (args.repo/'.git').exists(): run(['git','init','-b','main'],args.repo)
    status=subprocess.check_output(['git','status','--porcelain'],cwd=args.repo,text=True)
    if status.strip(): raise SystemExit('repository is not clean')
    for e in data['entries']:
        p=args.snapshot_dir/e['filename']
        with tempfile.TemporaryDirectory(prefix='pfes-import-') as td:
            ext=Path(td)
            with ZipFile(p) as zf: safe_extract(zf,ext)
            clear_worktree(args.repo)
            copy_tree(content_root(ext),args.repo)
        run(['git','add','-A'],args.repo)
        env=os.environ.copy()
        env.setdefault('GIT_AUTHOR_NAME','Project Frankenstein Steward')
        env.setdefault('GIT_AUTHOR_EMAIL','pfes@localhost')
        env.setdefault('GIT_COMMITTER_NAME',env['GIT_AUTHOR_NAME'])
        env.setdefault('GIT_COMMITTER_EMAIL',env['GIT_AUTHOR_EMAIL'])
        run(['git','commit','--allow-empty','-m',e['commit_message']],args.repo,env)
        run(['git','tag','-a',e['tag'],'-m',e['commit_message']],args.repo,env)
    run(['git','fsck','--full'],args.repo)
    print('local import complete; no remote push was performed')
if __name__=='__main__': main()
