import json
import os
import shutil
import sys
import argparse
import zipfile
import subprocess

# ==============================================================================
# 1. Messenger input CSV: Comes from a transformed exported CSV ("messenger_damien.csv").
#    Because standard Facebook exports exclude absolute timestamps in older epochs, dates
#    are parsed sequentially starting on Saturday, April 11, 2026. The JS layout engine
#    steps dates forward day-by-day as the relative weekday names change.
# 2. Instagram input dir: Parsed directly from a raw Instagram JSON export. Timestamps
#    are evaluated natively in milliseconds.
# 3. Direct quote replies: Compact network responses containing explicit message IDs 
#    are extracted from a browser network HAR capture using a targeted jq filter 
#    ("instagram_messagelist_reqs.json"). The JS compiler intersects these IDs with 
#    standard timestamps to draw linked quote cards in the message timeline.
# 4. Auditability: Raw source files are hosted unmodified in "static/inputs/" for user
#    validation. Visually redacted images are blurred out natively using CSS filters
#    rather than by modifying any of the input files.
# ==============================================================================

STATIC_PHOTOS_DIR = "static/photos"
STATIC_VIDEOS_DIR = "static/videos"

def process_and_copy_media(raw_path):
    if not raw_path: return ""
    raw_path = raw_path.strip()
    filename = os.path.basename(raw_path)
    
    if "your_instagram_activity" in raw_path:
        sub_dir = "photos" if "photos" in raw_path else "videos"
        dest_dir = STATIC_PHOTOS_DIR if sub_dir == "photos" else STATIC_VIDEOS_DIR
        
        os.makedirs(dest_dir, exist_ok=True)
        dest_path = os.path.join(dest_dir, filename)
        
        if os.path.exists(raw_path):
            try:
                shutil.copy2(raw_path, dest_path)
            except Exception:
                pass
            
        return f"static/{sub_dir}/{filename}"
    return raw_path

def copy_referenced_media(json_path):
    if not os.path.exists(json_path): return
    with open(json_path, 'r', encoding='utf-8') as f:
        data = json.load(f)
    for msg in data.get('messages', []):
        for key in ['photos', 'videos', 'audio_files']:
            if key in msg and msg[key]:
                uri = msg[key][0].get('uri', '')
                if uri:
                    process_and_copy_media(uri)

def merge_instagram_archive(zip_path):
    local_instagram_dir = "your_instagram_activity/messages/inbox/damien_542879920433437"
    local_json_path = os.path.join(local_instagram_dir, "message_1.json")
    
    existing_messages = []
    participants = []
    title = "Damien"
    thread_path = "messages/inbox/damien_542879920433437"
    
    if os.path.exists(local_json_path):
        try:
            with open(local_json_path, "r", encoding="utf-8") as f:
                old_data = json.load(f)
                existing_messages = old_data.get("messages", [])
                participants = old_data.get("participants", [])
                title = old_data.get("title", title)
                thread_path = old_data.get("thread_path", thread_path)
        except Exception as e:
            sys.stderr.write(f"Warning: Failed to load existing local messages: {e}\n")

    if not os.path.exists(zip_path):
        sys.stderr.write(f"Error: Zip file {zip_path} not found.\n")
        return

    sys.stderr.write(f"Processing archive: {zip_path}\n")
    new_messages = []

    with zipfile.ZipFile(zip_path, 'r') as z:
        for name in z.namelist():
            if "messages/inbox/damien_542879920433437" in name and name.endswith(".json") and "message_" in name:
                try:
                    with z.open(name) as f:
                        zip_data = json.loads(f.read().decode('utf-8'))
                        new_messages.extend(zip_data.get("messages", []))
                        if not participants:
                            participants = zip_data.get("participants", [])
                except Exception as e:
                    sys.stderr.write(f"Error reading JSON {name} from ZIP: {e}\n")
            
            is_media = False
            for m_dir in ["/photos/", "/videos/", "/audio/", "/files/"]:
                if "messages/inbox/damien_542879920433437" in name and m_dir in name:
                    is_media = True
                    break
            
            if is_media and not name.endswith('/'):
                suffix_index = name.find("messages/inbox/damien_542879920433437")
                relative_path = name[suffix_index:]
                dest_media_path = os.path.join("your_instagram_activity", relative_path)
                
                os.makedirs(os.path.dirname(dest_media_path), exist_ok=True)
                try:
                    with z.open(name) as src, open(dest_media_path, "wb") as dest:
                        shutil.copyfileobj(src, dest)
                except Exception as e:
                    sys.stderr.write(f"Error extracting media {name}: {e}\n")

    seen_keys = set()
    merged_messages = []
    
    def get_msg_key(m):
        content = m.get("content", "")
        media_key = ""
        for k in ["photos", "videos", "audio_files", "files"]:
            if k in m and m[k]:
                media_key += str(m[k][0].get("uri", ""))
        return (m.get("timestamp_ms"), m.get("sender_name"), content, media_key)

    for m in existing_messages:
        key = get_msg_key(m)
        if key not in seen_keys:
            seen_keys.add(key)
            merged_messages.append(m)

    added_count = 0
    for m in new_messages:
        key = get_msg_key(m)
        if key not in seen_keys:
            seen_keys.add(key)
            merged_messages.append(m)
            added_count += 1

    sys.stderr.write(f"Merged {len(existing_messages)} existing with {len(new_messages)} new messages. Added {added_count} new entries.\n")
    merged_messages.sort(key=lambda x: x.get("timestamp_ms", 0), reverse=True)
    
    os.makedirs(local_instagram_dir, exist_ok=True)
    output_data = {
        "participants": participants,
        "messages": merged_messages,
        "title": title,
        "is_still_participant": True,
        "thread_path": thread_path,
        "magic_words": []
    }
    with open(local_json_path, "w", encoding="utf-8") as f:
        json.dump(output_data, f, indent=2, ensure_ascii=False)

def build_dist_directory():
    """Isolates compiled deployment assets, excluding large non-essential raw workspace files."""
    sys.stderr.write("Building clean 'dist' folder for deployment...\n")
    dist_dir = "dist"
    if os.path.exists(dist_dir):
        shutil.rmtree(dist_dir)
    os.makedirs(dist_dir, exist_ok=True)
    
    # Copy compiled index.html
    if os.path.exists("index.html"):
        shutil.copy2("index.html", os.path.join(dist_dir, "index.html"))
        
    # Copy static assets folder recursively while filtering out items > 25MB
    if os.path.exists("static"):
        for root, dirs, files in os.walk("static"):
            rel_path = os.path.relpath(root, "static")
            dest_root = os.path.join(dist_dir, "static", rel_path) if rel_path != "." else os.path.join(dist_dir, "static")
            os.makedirs(dest_root, exist_ok=True)
            
            for f in files:
                src_file = os.path.join(root, f)
                dest_file = os.path.join(dest_root, f)
                
                # Exclude files exceeding Cloudflare's 25MB file limits
                file_size = os.path.getsize(src_file)
                if file_size > 25 * 1024 * 1024:
                    sys.stderr.write(f"Skipping {src_file} (exceeds 25MB limit: {file_size / (1024*1024):.1f} MB)\n")
                    continue
                    
                shutil.copy2(src_file, dest_file)

def deploy_test_version():
    if os.path.exists("instagram.zip"):
        sys.stderr.write("Uploading instagram.zip to R2 bucket 'cdn'...\n")
        try:
            subprocess.run(
                ["npx", "wrangler", "r2", "object", "put", "cdn/instagram.zip", "--file", "instagram.zip", "--remote"],
                check=True
            )
            sys.stderr.write("R2 Upload finished successfully.\n")
        except Exception as e:
            sys.stderr.write(f"Failed to upload asset to R2: {e}\n")
    else:
        sys.stderr.write("instagram.zip not found, skipping R2 upload step.\n")
        
    # Isolate static files in /dist folder
    build_dist_directory()
        
    sys.stderr.write("Registering new version on Cloudflare (Preview / Test environment)...\n")
    try:
        # Deploy with '--assets dist' to isolate compiled assets, passing compatibility date
        subprocess.run(
            ["npx", "wrangler", "versions", "upload", "--assets", "dist", "--name", "r-v-damien", "--compatibility-date", "2026-07-09"], 
            check=True
        )
        sys.stderr.write("Version successfully registered. Check output or dashboard for preview URL.\n")
    except Exception as e:
        sys.stderr.write(f"Upload command failed: {e}\n")

def promote_latest_version():
    sys.stderr.write("Promoting the latest test version to production...\n")
    try:
        result = subprocess.run(
            ["npx", "wrangler", "versions", "list", "--name", "r-v-damien", "--json"],
            capture_output=True, text=True, check=True
        )
        list_data = json.loads(result.stdout)
        if not list_data:
            sys.stderr.write("Error: No versions found to promote.\n")
            sys.exit(1)
            
        latest_version = max(list_data, key=lambda x: x.get('number', 0))
        latest_id = latest_version.get('id')
        latest_num = latest_version.get('number')
        
        if not latest_id:
            sys.stderr.write("Error: Could not determine latest version ID.\n")
            sys.exit(1)
            
        sys.stderr.write(f"Latest version found: #{latest_num} ({latest_id})\n")
        sys.stderr.write(f"Promoting version {latest_id} to 100% traffic...\n")
        
        subprocess.run(
            ["npx", "wrangler", "versions", "deploy", f"{latest_id}@100%", "--name", "r-v-damien", "--yes"],
            check=True
        )
        sys.stderr.write("Successfully promoted version to production.\n")
    except Exception as e:
        sys.stderr.write(f"Failed to promote version: {e}\n")
        sys.exit(1)

# Process arguments
parser = argparse.ArgumentParser(description="Compile logs and deployment workflows.")
parser.add_argument('--instagram', help='Path to new Instagram ZIP archive to append.')
parser.add_argument('--skip-deploy', action='store_true', help='Skip R2 upload and preview deployment.')
parser.add_argument('--promote', action='store_true', help='Promote the latest preview version directly to production (exits immediately).')
args, unknown = parser.parse_known_args()

if args.promote:
    promote_latest_version()
    sys.exit(0)

if args.instagram:
    merge_instagram_archive(args.instagram)

os.makedirs("static/inputs", exist_ok=True)

raw_messenger_path = "messenger_damien.csv"
raw_instagram_json = "your_instagram_activity/messages/inbox/damien_542879920433437/message_1.json"
raw_reqs_json = "instagram_messagelist_reqs.json"

shutil.copy2(__file__, "static/inputs/program.py")

if os.path.exists(raw_messenger_path):
    shutil.copy2(raw_messenger_path, "static/inputs/messenger.csv")

# Create instagram.zip from the current state of your_instagram_activity
instagram_folder = "your_instagram_activity/messages/inbox/damien_542879920433437"
if os.path.exists(instagram_folder):
    shutil.make_archive("instagram", 'zip', instagram_folder)

copy_referenced_media(raw_instagram_json)

if os.path.exists(raw_instagram_json):
    shutil.copy2(raw_instagram_json, "static/inputs/message_1.json")

if os.path.exists(raw_reqs_json):
    shutil.copy2(raw_reqs_json, "static/inputs/instagram_messagelist_reqs.json")

css_content = ""
if os.path.exists("style.css"):
    with open("style.css", "r", encoding="utf-8") as f:
        css_content = f.read()

HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>R v Damien McMullen</title>
    <script async defer src="https://calm-violet.swhra.uk"></script>
    <meta property="og:type" content="article">
    <meta property="og:title" content="R v Damien McMullen">
    <meta property="og:description" content="Chat message logs between Damien McMullen and Sam Robinson-Adams concerning Oliver Babington Wilson's double life, evolving criminal legal matters, and Damien McMullen's role in the affair.">
    <meta property="og:url" content="https://j-accuse.swhra.uk/">
    <meta property="og:site_name" content="R v Damien McMullen">
    <meta name="twitter:card" content="summary">
    <meta name="twitter:title" content="R v Damien McMullen">
    <meta name="twitter:description" content="Chat message logs between Damien McMullen and Sam Robinson-Adams concerning Oliver Babington Wilson's double life, evolving criminal legal matters, and Damien McMullen's role in the affair.">
    <meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large">
    <meta name="description" content="Chat message logs between Damien McMullen and Sam Robinson-Adams concerning Oliver Babington Wilson's double life, evolving criminal legal matters, and Damien McMullen's role in the affair.">
    <script src="//calm-violet.swhra.uk" async defer></script>
    <style>
{{CSS_CONTENT}}
        .embed-badge {
            position: fixed;
            bottom: 12px;
            right: 12px;
            background: #000000;
            color: #ffffff;
            padding: 6px 12px;
            border-radius: 6px;
            font-size: 10px;
            font-weight: 500;
            display: flex;
            align-items: center;
            gap: 10px;
            z-index: 9999;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
        }

        .embed-badge a {
            color: #3897f0;
            text-decoration: none;
            font-weight: 600;
        }

        .embed-badge a:hover {
            text-decoration: underline;
        }

        body.is-embed #top-bar {
            display: none;
        }

        body.is-embed .chat-area {
            padding-top: 10px;
        }
    </style>
</head>
<body>
    <div id="top-bar">
        <a href="static/inputs/program.py" class="nav-btn">Code</a>
        <a href="https://cdn.swhra.uk/instagram.zip" class="nav-btn">Instagram (ZIP)</a>
        <a href="static/inputs/messenger.csv" class="nav-btn">Messenger (CSV)</a>
        <a href="static/inputs/instagram_messagelist_reqs.json" class="nav-btn">Instagram API resps (JSON)</a>
        <button class="nav-btn" onclick="downloadChat()">Merged chat</button>
    </div>
    <main class="chat-area">
        <div id="chat-container">
            <div id="loading">Loading messages...</div>
        </div>
    </main>
    <script>
        // This script deliberately does all processing and rendering in the browser to avoid anything opaque to the user.
        // A tiny amount is done beforehand by a Python script, which is served at /static/inputs/program.py. You can fetch
        // it, along with the inputs /static/inputs/messenger.csv, /static/inputs/instagram.zip, and /static/inputs/instagram_messagelist_reqs.json,
        // and run the Python script yourself. It will produce exactly this HTML file and exactly that /static directory.
        // (Well, almost. Sorry. I excluded the videos from the ZIP file because they are too large, but you can fetch them from
        // /static/videos/1043129901614346.mp4, /static/videos/1321241956807105.mp4, /static/videos/1494448995816967.mp4,
        // /static/videos/1767621154404271.mp4, and /static/videos/2150642085508972.mp4 and add them to the unzipped 
        // your_instagram_activity/messages/inbox/damien_542879920433437/videos/ directory, and the script will THEN produce
        // exactly this HTML file and exactly that /static directory.) If you then open that HTML file in your browser
        // (file:///path/to/wherever/you/generated/it/index.html), it will render this website, with this script, with this comment.
        //
        // The inputs themselves are obtainable from Facebook and Instagram directly, WITH ONE EXCEPTION, which is the earlier
        // Messenger exchange. I can no longer obtain a proper raw export from Facebook because, on account of recent bloody
        // arrests, I am locked out of Facebook. (Yes, I know it is odd that Facebook and Instagram are separate despite both
        // being figments of Meta, but they are. I am logged in to Instagram and don't seem to require any 2FA to do an export,
        // but to get into Facebook requires 2FA tokens in the form of my phones and numbers which are now sadly immured.)
        // I have invited Damien to generate an export from his own account, which would really be the ideal solution here,
        // but he has not been forthcoming, probably because he fears I am tricking him into a trap. If he did so then I
        // would have a larger set of messages rather than a random old CSV I have had to rely on. I would welcome this,
        // but ... again, not forthcoming. Anyway.
        //
        // I feel I have gone to absurd lengths to make all of this reproducible, but you be the judge, which you can be
        // because I have gone to absurd lengths to make all of this reproducible.

        const platforms = ["WhatsApp", "iMessage", "Messenger", "Instagram"];
        const monthsShort = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
        
        const reactionEmoji = {
            "loved": "❤️", "liked": "👍", "disliked": "👎", 
            "laughed": "😂", "emphasized": "❗️", "questioned": "❓"
        };

        const REDACTED_MEDIA = [
            {
                path: "your_instagram_activity/messages/inbox/damien_542879920433437/photos/1734780957654325.jpg", 
                reason: "Removed at Damien's request."
            },
            {
                path: "your_instagram_activity/messages/inbox/damien_542879920433437/photos/860058433839627.jpg", 
                reason: "Removed because it's a hideous photo of me."
            }
        ];

        let chatData = [];
        const messageDOMMap = {};

        function cleanStr(str) {
            return str.replace(/[\u201c\u201d\u2018\u2019"']/g, '').replace(/\\s+/g, ' ').trim().toLowerCase();
        }

        function fixMojibake(val) {
            if (typeof val !== 'string') return val;
            try {
                const bytes = new Uint8Array(val.split('').map(c => c.charCodeAt(0)));
                return new TextDecoder('utf-8').decode(bytes);
            } catch (e) {
                return val;
            }
        }

        function getSenderId(name) {
            name = name.toLowerCase().trim();
            if (name === 'you' || name === 'sam robinson-adams') return 0;
            if (name === 'damien') return 1;
            return 2;
        }

        function getRedactedReason(text) {
            if (!text) return "";
            const filename = text.substring(text.lastIndexOf('/') + 1);
            const match = REDACTED_MEDIA.find(item => item.path.endsWith(filename));
            return match ? match.reason : "";
        }

        function parseCSV(text) {
            const lines = [];
            let row = [""];
            let inQuotes = false;
            for (let i = 0; i < text.length; i++) {
                const c = text[i];
                const next = text[i+1];
                if (c === '"') {
                    if (inQuotes && next === '"') {
                        row[row.length - 1] += '"';
                        i++;
                    } else {
                        inQuotes = !inQuotes;
                    }
                } else if (c === ',' && !inQuotes) {
                    row.push("");
                } else if ((c === '\\r' || c === '\\n') && !inQuotes) {
                    if (c === '\\r' && next === '\\n') i++;
                    lines.push(row);
                    row = [""];
                } else {
                    row[row.length - 1] += c;
                }
            }
            if (row.length > 1 || row[0] !== "") {
                lines.push(row);
            }
            return lines;
        }

        function processMediaLocation(path) {
            const filename = path.substring(path.lastIndexOf('/') + 1);
            let subDir = "photos";
            if (path.includes("videos")) subDir = "videos";
            if (path.includes("files")) subDir = "files";
            if (path.includes("audio")) subDir = "videos";
            return `static/${subDir}/${filename}`;
        }

        function processMessenger(rawCSV) {
            const rows = parseCSV(rawCSV);
            if (rows.length < 2) return [];
            
            const headers = rows[0].map(h => h.trim().toLowerCase());
            const timeIdx = headers.indexOf('time');
            const senderIdx = headers.indexOf('sender');
            const msgIdx = headers.indexOf('message');
            
            let baseDate = new Date(2026, 4, 9, 12, 0, 0);
            const weekdayMap = {
                'monday': 0, 'tuesday': 1, 'wednesday': 2, 'thursday': 3, 
                'friday': 4, 'saturday': 5, 'sunday': 6
            };

            const parsed = [];
            for (let i = 1; i < rows.length; i++) {
                const row = rows[i];
                if (row.length < 3) continue;
                
                const timeVal = row[timeIdx].trim();
                const sender = row[senderIdx].trim();
                const text = row[msgIdx].trim();
                
                const parts = timeVal.split(/\\s+/);
                if (parts.length === 2) {
                    const dayName = parts[0].toLowerCase();
                    const timeStr = parts[1];
                    const dayIdx = weekdayMap[dayName];
                    
                    if (dayIdx !== undefined) {
                        while (baseDate.getDay() !== (dayIdx + 1) % 7) {
                            baseDate.setDate(baseDate.getDate() + 1);
                        }
                        const [hour, minute] = timeStr.split(':').map(Number);
                        const msgDate = new Date(baseDate);
                        msgDate.setHours(hour, minute, 0, 0);
                        const ts = Math.floor(msgDate.getTime() / 1000);
                        
                        parsed.push([ts, getSenderId(sender), text, 0, 2, null, "", "", `msg-fb-${i}`, null, ""]);
                    }
                }
            }
            return parsed;
        }

        function processReqs(rawReqs) {
            if (!rawReqs) return {};
            const nodes = JSON.parse(rawReqs);
            const idMap = {};
            
            nodes.forEach(node => {
                if (!node) return;
                const ts_ms = parseInt(node.timestamp_ms);
                idMap[ts_ms] = {
                    id: node.id,
                    replyToId: node.replied_to_message_id,
                    repliedText: node.replied_to_message?.text_body || "",
                    repliedSender: node.replied_to_message?.sender?.id || node.replied_to_message?.sender_fbid || ""
                };
            });
            return idMap;
        }

        function processInstagram(rawJSON, reqsMap) {
            const data = JSON.parse(rawJSON);
            const parsed = [];
            
            data.messages.forEach(msg => {
                const sender = fixMojibake(msg.sender_name || "");
                const ts_ms = msg.timestamp_ms || 0;
                const ts = Math.floor(ts_ms / 1000);
                let content = fixMojibake(msg.content || "");
                
                let mediaPath = "";
                if (msg.photos && msg.photos.length > 0) {
                    mediaPath = msg.photos[0].uri;
                } else if (msg.videos && msg.videos.length > 0) {
                    mediaPath = msg.videos[0].uri;
                } else if (msg.audio_files && msg.audio_files.length > 0) {
                    mediaPath = msg.audio_files[0].uri;
                } else if (msg.files && msg.files.length > 0) {
                    mediaPath = msg.files[0].uri;
                }
                
                if (mediaPath) {
                    content = processMediaLocation(mediaPath);
                }
                
                const reqsMeta = reqsMap[ts_ms] || {};
                const msgId = reqsMeta.id || `msg-ig-${ts_ms}`;
                const replyToId = reqsMeta.replyToId || null;
                let replyText = reqsMeta.repliedText || "";
                let replySender = reqsMeta.repliedSender ? fixMojibake(reqsMeta.repliedSender) : "";
                
                const redactedReason = getRedactedReason(content);
                parsed.push([ts, getSenderId(sender), content, 0, 3, null, replyText, redactedReason, msgId, replyToId, replySender]);
            });
            return parsed;
        }

        async function decodeAndRender() {
            try {
                const [messengerRes, instagramRes, reqsRes] = await Promise.all([
                    fetch('static/inputs/messenger.csv'),
                    fetch('static/inputs/message_1.json'),
                    fetch('static/inputs/instagram_messagelist_reqs.json').catch(() => null)
                ]);

                if (!messengerRes.ok || !instagramRes.ok) {
                    throw new Error("Failed to fetch inputs.");
                }

                const messengerCSV = await messengerRes.text();
                const instagramJSON = await instagramRes.text();
                
                let reqsMap = {};
                if (reqsRes && reqsRes.ok) {
                    const reqsJSON = await reqsRes.text();
                    reqsMap = processReqs(reqsJSON);
                }

                const messengerData = processMessenger(messengerCSV);
                const instagramData = processInstagram(instagramJSON, reqsMap);

                chatData = [...messengerData, ...instagramData];
                chatData.sort((a, b) => a[0] - b[0]);

                preprocessReactions();
                renderChat();
            } catch (err) {
                document.getElementById('loading').innerText = "Error loading chat data.";
                console.error(err);
            }
        }

        function preprocessReactions() {
            const reactionRegex = /^(Loved|Liked|Disliked|Laughed at|Emphasized|Questioned)\\s+[“"'\u201c](.+?)[”"'\u201d]/i;

            for (let i = 0; i < chatData.length; i++) {
                const msg = chatData[i];
                const text = msg[2];
                const platformId = msg[4];

                if (platformId === 1 && text) {
                    const match = text.match(reactionRegex);
                    if (match) {
                        let reactionType = match[1].toLowerCase();
                        if (reactionType === "laughed at") reactionType = "laughed";
                        
                        const targetText = cleanStr(match[2]);
                        const senderName = msg[1] === 0 ? "Sam" : "Damien";

                        const limit = Math.max(0, i - 100);
                        for (let j = i - 1; j >= limit; j--) {
                            const prevMsg = chatData[j];
                            if (prevMsg[5] && prevMsg[5].isSystem) continue;

                            const prevTextClean = cleanStr(prevMsg[2]);
                            if (prevTextClean.includes(targetText) || targetText.includes(prevTextClean)) {
                                if (!prevMsg.reactions) prevMsg.reactions = [];
                                prevMsg.reactions.push({ type: reactionType, sender: senderName });
                                break;
                            }
                        }

                        msg[5] = {
                            isSystem: true,
                            systemText: `${senderName} ${match[1].toLowerCase()} a message`
                        };
                    }
                }
            }
        }

        function renderMarkdown(text) {
            if (!text) return "";
            let escaped = text
                .replace(/&/g, "&amp;")
                .replace(/</g, "&lt;")
                .replace(/>/g, "&gt;")
                .replace(/"/g, "&quot;")
                .replace(/'/g, "&#39;");
            escaped = escaped.replace(/`(?!\\\\s)([^`]+?)(?<!\\\\s)`/g, "<code>$1</code>");
            escaped = escaped.replace(/\\*\\*(?!\\\\s)([^\\*]+?)(?<!\\\\s)\\*\\*/g, "<strong>$1</strong>");
            escaped = escaped.replace(/\\\\*(?!\\\\s)([^\\\\*]+?)(?<!\\\\s)\\\\*\\\\/g, "<strong>$1</strong>");
            escaped = escaped.replace(/_(?!\\\\s)([^_]+?)(?<!\\\\s)_/g, "<em>$1</em>");
            return escaped;
        }

        window.toggleExpand = function(btn) {
            const textDiv = btn.previousElementSibling;
            if (textDiv.classList.contains('collapsed')) {
                textDiv.classList.remove('collapsed');
                btn.innerText = 'Show less';
            } else {
                textDiv.classList.add('collapsed');
                btn.innerText = 'See more';
            }
        };

        window.copyHash = function(event, id) {
            event.stopPropagation();
            const url = window.location.origin + window.location.pathname + '#' + id;
            navigator.clipboard.writeText(url).then(() => {
                const tooltip = event.currentTarget.querySelector('.copy-tooltip');
                tooltip.classList.add('show');
                setTimeout(() => tooltip.classList.remove('show'), 1500);
            });
        };

        window.scrollToMessage = function(event, id) {
            event.preventDefault();
            event.stopPropagation();
            const target = document.getElementById(id);
            if (target) {
                target.scrollIntoView({ behavior: 'auto', block: 'center' });
                target.classList.add('target-highlight');
                setTimeout(() => target.classList.remove('target-highlight'), 2000);
            }
        };

        function renderChat() {
            const container = document.getElementById('chat-container');
            container.innerHTML = ''; 
            
            let currentMonthKey = null;
            let currentYear = null;
            
            let lastSenderId = null;
            let lastTs = null;
            let lastDateStr = null;
            let lastPrintedPlatformId = null;
            
            const fragment = document.createDocumentFragment();

            const urlParams = new URLSearchParams(window.location.search);
            const embedId = urlParams.get('id');
            const ctxParam = urlParams.get('ctx');
            const isEmbed = window.location.pathname.includes('/embed') || urlParams.has('embed') || !!embedId;

            let messagesToRender = chatData;
            if (isEmbed && embedId) {
                const targetIdx = chatData.findIndex(msg => msg[8] === embedId);
                if (targetIdx !== -1) {
                    const ctx = parseInt(ctxParam, 10);
                    const actualCtx = isNaN(ctx) ? 0 : ctx;
                    const start = Math.max(0, targetIdx - actualCtx);
                    const end = Math.min(chatData.length, targetIdx + actualCtx + 1);
                    messagesToRender = chatData.slice(start, end);
                }

                document.body.classList.add('is-embed');
                const badge = document.createElement('div');
                badge.className = 'embed-badge';
                
                let cleanPath = window.location.pathname;
                if (cleanPath.endsWith('/embed')) {
                    cleanPath = cleanPath.slice(0, -6) + '/';
                }
                const goToUrl = window.location.origin + cleanPath + '#' + embedId;
                badge.innerHTML = `<span>damien</span> <a href="${goToUrl}" target="_blank">Go to chat</a>`;
                document.body.appendChild(badge);
            }

            messagesToRender.forEach((msg, index) => {
                const [ts, senderId, text, approxType, platformId, sysMeta, replyText, redactedReason, msgId, replyToId, replySender] = msg;
                const date = new Date(ts * 1000);
                const year = date.getFullYear();
                const month = date.getMonth();
                const day = date.getDate();
                const monthKey = `${year}-${String(month).padStart(2, '0')}`;

                if (monthKey !== currentMonthKey) {
                    currentMonthKey = monthKey;
                    lastSenderId = null;
                    
                    const div = document.createElement('div');
                    div.className = 'month-divider';
                    div.id = 'month-' + monthKey;
                    div.innerText = `${monthsShort[month]} ${year}`;
                    fragment.appendChild(div);
                }

                const isConsecutive = (lastSenderId === senderId) && (ts - lastTs < 900);
                
                let isLastInCluster = true;
                const nextMsg = messagesToRender[index + 1];
                if (nextMsg) {
                    const nextSenderId = nextMsg[1];
                    const nextTs = nextMsg[0];
                    const nextSys = nextMsg[5];
                    if (nextSenderId === senderId && (!nextSys || !nextSys.isSystem) && (nextTs - ts < 900)) {
                        isLastInCluster = false;
                    }
                }

                lastSenderId = senderId;
                lastTs = ts;

                const platformName = platforms[platformId] || "Chat";
                const currentDateStr = `${year}-${month}-${day}`;
                const hours = String(date.getHours()).padStart(2, '0');
                const minutes = String(date.getMinutes()).padStart(2, '0');
                
                let dateStr = "";
                let displayPlatform = "";

                if (approxType > 0) {
                    dateStr = approxType === 1 ? `${monthsShort[month]} ${year} (estimated)` : `Mar 27, 2026 (estimated)`;
                    displayPlatform = `<span class="platform-badge">${platformName}</span>`;
                } else {
                    if (isLastInCluster) {
                        dateStr = `${day} ${monthsShort[month]} ${year}, ${hours}:${minutes}`;
                        displayPlatform = `<span class="platform-badge">${platformName}</span>`;
                    } else {
                        if (currentDateStr === lastDateStr) {
                            dateStr = `${hours}:${minutes}`;
                        } else {
                            dateStr = `${day} ${monthsShort[month]} ${year}, ${hours}:${minutes}`;
                        }
                        displayPlatform = "";
                    }
                }

                if (text && text.startsWith('https://us05web.zoom.us/j/89611645053')) {
                    const callDiv = document.createElement('div');
                    callDiv.className = 'system-event call-event';
                    callDiv.id = msgId;
                    callDiv.innerHTML = `
                        <div class="call-header">Video Call • ${dateStr}</div>
                        <div class="video-container">
                            <iframe src="https://www.youtube.com/embed/YFiezPJ4Dc8" frameborder="0" allowfullscreen style="width: 100%; height: 100%; border: none;"></iframe>
                        </div>`;
                    fragment.appendChild(callDiv);
                    lastSenderId = null;
                    return;
                }

                if (sysMeta && sysMeta.isSystem) {
                    const sysDiv = document.createElement('div');
                    sysDiv.className = 'system-event';
                    sysDiv.id = msgId;
                    sysDiv.innerText = `${sysMeta.systemText} • ${dateStr}`;
                    fragment.appendChild(sysDiv);
                    lastSenderId = null;
                    return;
                }

                const isLocalImage = text && text.startsWith('static/photos/');
                const isLocalVideo = text && text.startsWith('static/videos/');
                const isLocalFile = text && text.startsWith('static/files/');
                const isDriveLink = text && text.includes('drive.google.com');

                let classNames = 'message ' + (senderId === 0 ? 'sam' : 'correspondent');
                if (isConsecutive) classNames += ' consecutive';
                if (!isLastInCluster) classNames += ' not-last';
                if (isLocalImage || isLocalVideo || isLocalFile || isDriveLink) classNames += ' media-only';

                const msgDiv = document.createElement('div');
                msgDiv.className = classNames;
                msgDiv.id = msgId;

                const bubbleContainer = document.createElement('div');
                bubbleContainer.className = 'bubble-container';

                const bubble = document.createElement('div');
                bubble.className = 'bubble';

                if (replyText) {
                    const quoteDiv = document.createElement('div');
                    quoteDiv.className = 'quote-reply';
                    const truncatedReply = replyText.length > 100 ? (replyText.substring(0, 100) + '...') : replyText;
                    const formattedReply = renderMarkdown(truncatedReply);
                    
                    if (replyToId) {
                        quoteDiv.onclick = (e) => window.scrollToMessage(e, replyToId);
                        quoteDiv.innerHTML = `<span class="quote-text">${formattedReply}</span>`;
                    } else {
                        quoteDiv.innerHTML = formattedReply;
                    }
                    bubbleContainer.appendChild(quoteDiv);
                }

                if (isLocalImage) {
                    if (redactedReason) {
                        bubble.innerHTML += `
                            <div class="redacted-media-wrapper">
                                <div class="redacted-overlay">
                                    <span class="redacted-message">${redactedReason}</span>
                                </div>
                                <img src="${text}" alt="Local Photo" />
                            </div>`;
                    } else {
                        bubble.innerHTML += `<img src="${text}" alt="Local Photo" />`;
                    }
                } else if (isLocalVideo) {
                    if (redactedReason) {
                        bubble.innerHTML += `<div class="redacted-media-wrapper">
                            <div class="redacted-overlay">
                                <span class="redacted-message">${redactedReason}</span>
                            </div>
                            <div class="mock-video"></div>
                        </div>`;
                    } else {
                        bubble.innerHTML += `<video src="${text}" controls></video>`;
                    }
                } else if (isLocalFile) {
                    const filename = text.substring(text.lastIndexOf('/') + 1);
                    bubble.innerHTML += `<a href="${text}" target="_blank" class="drive-card">
                        <svg class="drive-logo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width: 32px; height: 32px; color: #5f6368;"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" y2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
                        <div class="drive-info">
                            <span class="drive-title">${filename}</span>
                            <span class="drive-subtitle">Document</span>
                        </div>
                    </a>`;
                } else if (isDriveLink) {
                    const fileIdMatch = text.match(/file\\/d\\/([^\\/\\?]+)/);
                    const fileId = fileIdMatch ? fileIdMatch[1] : 'Asset';
                    bubble.innerHTML += `<a href="${text}" target="_blank" class="drive-card">
                        <img class="drive-logo" src="https://www.gstatic.com/images/branding/productlogos/drive_2026/v1/web-48dp/logo_drive_2026_color_2x_web_48dp.png" alt="Google Drive" />
                        <div class="drive-info">
                            <span class="drive-title">${fileId}</span>
                            <span class="drive-subtitle">drive.google.com</span>
                        </div>
                    </a>`;
                } else {
                    const isLongText = text && (text.length > 1200 || text.split(/\\r\\n|\\r|\\n/).length > 14);
                    
                    const textDiv = document.createElement('div');
                    textDiv.className = isLongText ? 'text-content collapsed' : 'text-content';
                    textDiv.innerHTML = renderMarkdown(text);
                    bubble.appendChild(textDiv);
                    
                    if (isLongText) {
                        const expandBtn = document.createElement('div');
                        expandBtn.className = 'expand-btn';
                        expandBtn.innerText = 'See more';
                        expandBtn.onclick = function() { window.toggleExpand(this); };
                        bubble.appendChild(expandBtn);
                    }
                }

                bubbleContainer.appendChild(bubble);

                const copyBtn = document.createElement('div');
                copyBtn.className = 'copy-btn';
                copyBtn.onclick = (e) => window.copyHash(e, msgId);
                copyBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg><span class="copy-tooltip">Copied!</span>`;
                bubbleContainer.appendChild(copyBtn);

                if (msg.reactions && msg.reactions.length > 0) {
                    const badgeGroup = document.createElement('div');
                    badgeGroup.className = 'reaction-badge-group';
                    const uniqueEmojis = [...new Set(msg.reactions.map(r => reactionEmoji[r.type] || ""))].join("");
                    badgeGroup.innerText = uniqueEmojis;
                    bubbleContainer.appendChild(badgeGroup);
                }

                msgDiv.appendChild(bubbleContainer);

                const metaDiv = document.createElement('div');
                metaDiv.className = 'metadata' + (approxType > 0 ? ' approximate' : '');
                metaDiv.innerHTML = `<span>${dateStr}</span> ${displayPlatform}`;
                msgDiv.appendChild(metaDiv);

                lastDateStr = approxType > 0 ? null : currentDateStr;
                lastPrintedPlatformId = platformId;
                
                messageDOMMap[msgId] = {
                    sender: senderId === 0 ? "Sam" : "Damien",
                    text: isLocalImage || isLocalVideo || isLocalFile || isDriveLink ? "[attachment]" : text
                };

                fragment.appendChild(msgDiv);
            });

            container.appendChild(fragment);

            const hashId = window.location.hash ? window.location.hash.substring(1) : embedId;
            if (hashId) {
                const target = document.getElementById(hashId);
                if (target) {
                    target.scrollIntoView({ behavior: 'auto', block: 'center' });
                    target.classList.add('target-highlight');
                }
            }
        }

        async function downloadChat() {
            try {
                const serializedChat = JSON.stringify(chatData.map(msg => {
                    const [timestamp, senderId, content, approxType, platformId, sysMeta, replyText, redactedReason, msgId, replyToId, replySender] = msg;
                    return {
                        timestamp,
                        sender: senderId === 0 ? "Sam" : "Damien",
                        content,
                        platform: platforms[platformId],
                        isSystem: sysMeta && sysMeta.isSystem ? true : false,
                        systemText: sysMeta && sysMeta.systemText ? sysMeta.systemText : "",
                        replyText,
                        redactedReason,
                        msgId,
                        replyToId,
                        replySender,
                    };
                }), null, 2);
                const blob = new Blob([serializedChat], { type: 'application/json' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = 'chat.json';
                a.click();
                URL.revokeObjectURL(url);
            } catch (err) {
                alert("Error downloading chat data: " + err.message);
            }
        }

        decodeAndRender();
    </script>
</body>
</html>
"""

final_html = (
    HTML_TEMPLATE
    .replace("{{CSS_CONTENT}}", css_content)
)

with open("index.html", "w", encoding="utf-8") as f:
    f.write(final_html)

if not args.skip_deploy:
    deploy_test_version()