import json
import os
import shutil
import sys

# ==============================================================================
# 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)

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")
instagram_folder = "your_instagram_activity/messages/inbox/damien_542879920433437"
if os.path.exists(instagram_folder):
    shutil.make_archive("static/inputs/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</title>
    <script src="https://calm-violet.samuelwilliamhowardrobinsonadams.uk" async defer></script>
    <style>
{{CSS_CONTENT}}
        code {
            font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
            background-color: rgba(0, 0, 0, 0.08);
            padding: 2px 4px;
            border-radius: 4px;
            font-size: 0.9em;
        }
    </style>
</head>
<body>
    <div id="top-bar">
        <a href="static/inputs/program.py" class="nav-btn">Program source</a>
        <a href="https://cdn.swhra.uk/instagram.zip" class="nav-btn">Instagram export 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 reqs JSON</a>
        <button class="nav-btn" onclick="downloadChat()">Final processed log</button>
    </div>

    <main class="chat-area">
        <div id="chat-container">
            <div id="loading">Decompressing chat...</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_text || "",
                    repliedSender: node.replied_to_sender || ""
                };
            });
            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 "";
            
            // Step 1: Escape HTML characters to protect against injection
            let escaped = text
                .replace(/&/g, "&amp;")
                .replace(/</g, "&lt;")
                .replace(/>/g, "&gt;")
                .replace(/"/g, "&quot;")
                .replace(/'/g, "&#39;");

            // Step 2: Render backticks as monospace (without leading/trailing spaces)
            escaped = escaped.replace(/`(?!\\s)([^`]+?)(?<!\\s)`/g, "<code>$1</code>");

            // Step 3: Render double asterisks as bold (without leading/trailing spaces)
            escaped = escaped.replace(/\\*\\*(?!\\s)([^\\*]+?)(?<!\\s)\\*\\*/g, "<strong>$1</strong>");

            // Step 4: Render single asterisks as bold (without leading/trailing spaces)
            escaped = escaped.replace(/\\*(?!\\s)([^\\*]+?)(?<!\\s)\\*/g, "<strong>$1</strong>");

            // Step 5: Render single underscores as italics (without leading/trailing spaces)
            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();

            chatData.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 = chatData[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 ? "[Media Attachment]" : text
                };

                fragment.appendChild(msgDiv);
            });

            container.appendChild(fragment);

            if (window.location.hash) {
                const hashId = window.location.hash.substring(1);
                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)
)

sys.stdout.write(final_html)