#!/usr/bin/env python3
"""Rebuild project files from the Codex Desktop sessions by replaying apply_patch calls and heredoc writes.

usage: replay_codex_sessions.py OUT_DIR
Streams ~/.codex/sessions and ~/.codex/archived_sessions (never copies them), orders the patches by time and writes the
reconstructed files that are NOT on disk now (plus a report of failed hunks) to OUT_DIR.
"""
import glob
import json
import os
import re
import sys

ROOT = '/Users/chudy/Library/Application Support/Kodi/addons/plugin.video.milionar/'
CODEX = os.path.expanduser('~/.codex/')
MARK = 'Kodi/addons/plugin.video.milionar'


def sessions():
    files = glob.glob(CODEX + 'sessions/*/*/*/*.jsonl') + glob.glob(CODEX + 'archived_sessions/**/*.jsonl', recursive=True)
    found = []
    for path in files:
        with open(path, 'rb') as handle:
            hit = False
            for chunk in iter(lambda: handle.read(1 << 24), b''):
                if MARK.encode() in chunk:
                    hit = True
                    break
        if hit:
            found.append(path)
    return found


def js_strings_after(text, marker):
    """Yield JS string literals that are the first argument of `marker(`."""
    pos = 0
    while True:
        index = text.find(marker, pos)
        if index < 0:
            return
        i = index + len(marker)
        while i < len(text) and text[i] in ' \n\t':
            i += 1
        if i >= len(text) or text[i] not in '"\'`':
            pos = i
            continue
        quote = text[i]
        j = i + 1
        while j < len(text):
            if text[j] == '\\':
                j += 2
                continue
            if text[j] == quote:
                break
            j += 1
        raw = text[i + 1:j]
        pos = j + 1
        try:
            if quote == '"':
                yield json.loads('"' + raw + '"')
            else:
                yield raw.encode('utf-8').decode('unicode_escape') if '\\' in raw else raw
        except Exception:
            continue


def parse_patch(text):
    """-> list of ops (kind, path, extra) from an apply_patch body."""
    ops, lines, i = [], text.split('\n'), 0
    current = None
    while i < len(lines):
        line = lines[i]
        if line.startswith('*** Add File: '):
            current = ['add', line[14:].strip(), []]
            ops.append(current)
        elif line.startswith('*** Update File: '):
            current = ['update', line[17:].strip(), [], None]
            ops.append(current)
        elif line.startswith('*** Delete File: '):
            ops.append(['delete', line[17:].strip(), []])
            current = None
        elif line.startswith('*** Move to: ') and current and current[0] == 'update':
            current[3] = line[13:].strip()
        elif line.startswith('*** End Patch') or line.startswith('*** Begin Patch'):
            current = None
        elif current is not None:
            current[2].append(line)
        i += 1
    return ops


def rel(path):
    path = path.strip()
    return path[len(ROOT):] if path.startswith(ROOT) else path.lstrip('/') if path.startswith('/') else path


def apply_update(content, body):
    hunks, hunk = [], []
    for line in body:
        if line.startswith('@@'):
            if hunk:
                hunks.append(hunk)
            hunk = []
        else:
            hunk.append(line)
    if hunk:
        hunks.append(hunk)
    lines = content.split('\n')
    cursor = 0
    for hunk in hunks:
        old = [l[1:] for l in hunk if l[:1] in (' ', '-')]
        new = [l[1:] for l in hunk if l[:1] in (' ', '+')]
        if not old:                                   # pure insertion at the end
            lines[len(lines):] = new
            continue
        found = -1
        for start in list(range(cursor, len(lines) - len(old) + 1)) + list(range(0, cursor)):
            if all(lines[start + k].rstrip() == old[k].rstrip() for k in range(len(old))):
                found = start
                break
        if found < 0:
            return None
        lines[found:found + len(old)] = new
        cursor = found + len(new)
    return '\n'.join(lines)


def main(out):
    events = []
    for path in sessions():
        with open(path, encoding='utf-8', errors='ignore') as handle:
            for number, line in enumerate(handle):
                if 'apply_patch' not in line and 'EOF' not in line and "<<'" not in line:
                    continue
                try:
                    record = json.loads(line)
                except ValueError:
                    continue
                payload = record.get('payload') or {}
                text = payload.get('input') if isinstance(payload.get('input'), str) else payload.get('arguments')
                if not isinstance(text, str):
                    continue
                events.append((record.get('timestamp') or '', os.path.basename(path), number, text))
    events.sort()
    vfs, failed, log = {}, [], []
    for stamp, name, number, text in events:
        patches = list(js_strings_after(text, 'apply_patch(')) + [m for m in re.findall(r'\*\*\* Begin Patch.*?\*\*\* End Patch', text, re.S)
                                                                  if '\\n' not in m]
        for body in patches:
            if '*** Begin Patch' not in body:
                continue
            for op in parse_patch(body):
                kind, path = op[0], rel(op[1])
                if kind == 'add':
                    vfs[path] = '\n'.join(l[1:] if l.startswith('+') else l for l in op[2]).rstrip('\n') + '\n'
                elif kind == 'delete':
                    vfs.pop(path, None)
                elif kind == 'update':
                    if path not in vfs:
                        failed.append((stamp, name, path, 'no base'))
                        continue
                    result = apply_update(vfs[path], op[2])
                    if result is None:
                        failed.append((stamp, name, path, 'hunk mismatch'))
                        continue
                    vfs[path] = result
                    if op[3]:
                        vfs[rel(op[3])] = vfs.pop(path)
        for match in re.finditer(r"cat > ?(\S+) <<'?(\w+)'?\n(.*?)\n\2\b", text.replace('\\n', '\n'), re.S):
            vfs[rel(match.group(1).strip('"\''))] = match.group(3) + '\n'
    os.makedirs(out, exist_ok=True)
    report = open(os.path.join(out, 'REPORT.txt'), 'w')
    kept = 0
    for path, content in sorted(vfs.items()):
        if os.path.exists(ROOT + path):
            report.write('on-disk-now\t' + path + '\n')
            continue
        target = os.path.join(out, 'files', path)
        os.makedirs(os.path.dirname(target), exist_ok=True)
        with open(target, 'w', encoding='utf-8') as handle:
            handle.write(content)
        report.write('recovered\t{}\t{}\n'.format(path, len(content)))
        kept += 1
    for item in failed:
        report.write('FAILED\t' + '\t'.join(map(str, item)) + '\n')
    report.close()
    print('patch events', len(events), 'files in vfs', len(vfs), 'recovered (not on disk)', kept, 'failed hunks', len(failed))


if __name__ == '__main__':
    main(sys.argv[1])
