package main

import (
	"archive/zip"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"strings"
)

type playworksConvertOptions struct {
	SourcePath string   `json:"sourcePath"`
	OutputDir  string   `json:"outputDir"`
	BaseName   string   `json:"baseName"`
	Networks   []string `json:"networks"`
}

type playworksNetworkResult struct {
	Network string `json:"network"`
	File    string `json:"file"`
	OK      bool   `json:"ok"`
	Error   string `json:"error,omitempty"`
}

func PlayworksPage(w http.ResponseWriter, r *http.Request) {
	body := `
<header class="tool-header">
  <h2 class="tool-title">Playworks Converter</h2>
  <p class="tool-description">Convert a Playworks HTML export into network-specific playable outputs.</p>
</header>

<style>
  .net-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(250px,1fr)); gap:4px 12px; }
  .net-row { display:flex; align-items:center; gap:8px; padding:6px 8px; cursor:pointer; user-select:none; border:1px solid #333; border-radius:4px; background:rgba(128,128,128,0.06); }
  .net-label { min-width:100px; font-weight:500; }
  .net-tag { font-size:10px; opacity:0.6; font-weight:400; }
  .net-note { font-size:11px; opacity:0.6; }
  .toggle-row { display:flex; gap:8px; margin-bottom:6px; }
  .toggle-row button { font-size:11px; padding:2px 8px; }
</style>

<section class="tool-panel">
  <div class="panel-header"><h3 class="panel-title">Inputs</h3></div>
  <div class="panel-body">
  <div class="control-group">
  <div class="control-label">Source HTML (Playworks export)</div>
  <div id="dropZone" class="drop-zone">
    <div class="file-row">
      <button id="pickSrc" class="secondary">Select File</button>
      <button id="clearSrc" class="secondary">Clear</button>
    </div>
    <div class="drop-zone-text">or drop a Playworks HTML file here</div>
  </div>
  <input id="src" type="hidden" />
  <div id="sourceSelection" class="selected-list"></div>
  </div>

  <div class="control-group">
  <div class="control-label">Base Filename</div>
  <div class="field-row">
    <input id="baseName" type="text" placeholder="Base filename..." style="flex:1;" />
  </div>
  </div>

  <div class="control-group">
  <div class="control-label">Output Folder</div>
  <div class="field-row">
    <input id="outDir" type="text" placeholder="Output folder..." style="flex:1;" />
    <button id="pickOut" class="secondary">Select Folder</button>
  </div>
  </div>

  <div class="control-group">
  <div class="control-label">Networks to generate</div>
  <div class="toggle-row">
    <button id="selectAll" class="secondary">All</button>
    <button id="selectNone" class="secondary">None</button>
  </div>
  <div class="net-grid">
    <label class="net-row"><input type="checkbox" class="net-cb" data-tag="al" checked /><span class="net-label">Applovin <span class="net-tag">al</span></span><span class="net-note">HTML + mraid.js in head</span></label>
    <label class="net-row"><input type="checkbox" class="net-cb" data-tag="is" checked /><span class="net-label">Ironsource <span class="net-tag">is</span></span><span class="net-note">HTML + mraid.js in head</span></label>
    <label class="net-row"><input type="checkbox" class="net-cb" data-tag="fb" checked /><span class="net-label">Facebook <span class="net-tag">fb</span></span><span class="net-note">HTML</span></label>
    <label class="net-row"><input type="checkbox" class="net-cb" data-tag="gg" checked /><span class="net-label">Google Ads <span class="net-tag">gg</span></span><span class="net-note">ZIP + exitapi.js in head</span></label>
    <label class="net-row"><input type="checkbox" class="net-cb" data-tag="ap" checked /><span class="net-label">Appreciate <span class="net-tag">ap</span></span><span class="net-note">ZIP + exitapi.js in head</span></label>
    <label class="net-row"><input type="checkbox" class="net-cb" data-tag="mo" checked /><span class="net-label">Moloco <span class="net-tag">mo</span></span><span class="net-note">HTML + FbPlayableAd CTA</span></label>
    <label class="net-row"><input type="checkbox" class="net-cb" data-tag="vu" checked /><span class="net-label">Vungle <span class="net-tag">vu</span></span><span class="net-note">ZIP + __VUNGLE__ flag</span></label>
    <label class="net-row"><input type="checkbox" class="net-cb" data-tag="mtg" checked /><span class="net-label">Mintegral <span class="net-tag">mtg</span></span><span class="net-note">ZIP + onload gameReady</span></label>
    <label class="net-row"><input type="checkbox" class="net-cb" data-tag="tt" /><span class="net-label">TikTok <span class="net-tag">tt</span></span><span class="net-note">HTML + __TIKTOK__ flag</span></label>
  </div>
  </div>

<div class="action-row">
  <button id="convert" disabled>Convert</button>
  <button id="openOut" class="secondary" disabled>Open Output Folder</button>
</div>

<div id="status" class="status-panel"></div>
</div>
</section>
<div id="results" class="results-panel"></div>

<script>
let outputDir = '';
const srcEl = document.getElementById('src');
const outEl = document.getElementById('outDir');
const baseNameEl = document.getElementById('baseName');
const convertBtn = document.getElementById('convert');
const openOutBtn = document.getElementById('openOut');
const statusEl = document.getElementById('status');
const resultsEl = document.getElementById('results');
const sourceSelectionEl = document.getElementById('sourceSelection');

async function readJSONStream(response, handle) {
  if (!response.ok || !response.body) throw new Error('Request failed.');
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    let newline;
    while ((newline = buffer.indexOf('\n')) >= 0) {
      const line = buffer.slice(0, newline);
      buffer = buffer.slice(newline + 1);
      if (line.trim()) handle(JSON.parse(line));
    }
  }
  if (buffer.trim()) handle(JSON.parse(buffer));
}

function updateConvertBtn() {
  const hasNet = [...document.querySelectorAll('.net-cb')].some(c => c.checked);
  convertBtn.disabled = !srcEl.value || !outEl.value || !baseNameEl.value.trim() || !hasNet;
}

function basename(p) {
  const i = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\'));
  return i >= 0 ? p.slice(i + 1) : p;
}

function escapeHtml(s){
  return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}

function renderSourceSelection() {
  if (!srcEl.value) {
    sourceSelectionEl.innerHTML = '<span class="file-name">(no file selected)</span>';
    return;
  }
  sourceSelectionEl.innerHTML = '<div class="results-panel" style="margin-top:0;"><table class="data-table">' +
    '<thead><tr><th>Filename</th><th style="width:44px;"></th></tr></thead><tbody>' +
    '<tr><td class="mono wrap">' + escapeHtml(basename(srcEl.value)) + '</td><td><button id="removeSource" class="remove-selected danger" title="Remove">&times;</button></td></tr>' +
    '</tbody></table></div>';
  document.getElementById('removeSource').addEventListener('click', clearSource);
}

function setSource(path, baseName, dir) {
  srcEl.value = path || '';
  baseNameEl.value = baseName || '';
  if (!outEl.value && dir) { outEl.value = defaultOutputDir(dir); outputDir = outEl.value; }
  renderSourceSelection();
  updateConvertBtn();
}

function defaultOutputDir(dir) {
  return dir ? dir.replace(/[\\\/]+$/, '') + '\\Output' : '';
}

function clearSource() {
  srcEl.value = '';
  baseNameEl.value = '';
  statusEl.textContent = '';
  statusEl.classList.remove('is-busy');
  renderSourceSelection();
  updateConvertBtn();
}

document.getElementById('pickSrc').addEventListener('click', async () => {
  const r = await fetch('/api/playworks/pickSource', { method:'POST' });
  const j = await r.json();
  if (j.path) {
    setSource(j.path, j.baseName || '', j.dir || '');
  }
});
document.getElementById('clearSrc').addEventListener('click', clearSource);
document.getElementById('pickOut').addEventListener('click', async () => {
  const r = await fetch('/api/playworks/pickOutput', { method:'POST' });
  const j = await r.json();
  if (j.path) { outEl.value = j.path; outputDir = j.path; updateConvertBtn(); }
});
document.getElementById('selectAll').addEventListener('click', () => {
  document.querySelectorAll('.net-cb').forEach(c => c.checked = c.dataset.tag !== 'tt');
  updateConvertBtn();
});
document.getElementById('selectNone').addEventListener('click', () => {
  document.querySelectorAll('.net-cb').forEach(c => c.checked = false);
  updateConvertBtn();
});
document.querySelectorAll('.net-cb').forEach(c => c.addEventListener('change', updateConvertBtn));
outEl.addEventListener('input', updateConvertBtn);
baseNameEl.addEventListener('input', updateConvertBtn);
openOutBtn.addEventListener('click', () => {
  if (outputDir) fetch('/api/open', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ url: outputDir }) });
});

function outputDisplayName(file) {
  const parts = file.split(/[\\\\/]/).filter(Boolean);
  return parts.slice(-2).join('/');
}
setupDropZone('dropZone', statusEl, (j) => {
  const paths = j.paths || [];
  if (!paths.length) {
    if (j.skipped) statusEl.textContent = 'Skipped ' + j.skipped + ' non-HTML or unavailable item(s).';
    return;
  }
  const sourcePath = paths[0];
  const droppedBase = basename(sourcePath).replace(/\\.html?$/i, '').replace(/_(?:un|al|is|fb|gg|ap|mo|vu|mtg|tt)$/i, '');
  const i = Math.max(sourcePath.lastIndexOf('/'), sourcePath.lastIndexOf('\\\\'));
  setSource(sourcePath, droppedBase, i >= 0 ? sourcePath.slice(0, i) : '');
  const skipped = Number(j.skipped || 0) + Math.max(0, paths.length - 1);
  if (skipped) statusEl.textContent = 'Using first dropped HTML file. Skipped ' + skipped + ' other item(s).';
});
renderSourceSelection();

document.getElementById('convert').addEventListener('click', async () => {
  const networks = [...document.querySelectorAll('.net-cb')].filter(c => c.checked).map(c => c.dataset.tag);
  statusEl.textContent = 'Converting...';
  statusEl.classList.add('is-busy');
  resultsEl.innerHTML = '';
  openOutBtn.disabled = true;
  convertBtn.disabled = true;
  const res = await fetch('/api/playworks/convert', {
    method:'POST',
    headers:{'Content-Type':'application/json'},
    body: JSON.stringify({ sourcePath: srcEl.value, outputDir: outEl.value, baseName: baseNameEl.value, networks })
  });
  let msg = null;
  try {
    await readJSONStream(res, (event) => {
      if (event.type === 'status') {
        statusEl.textContent = event.message;
        statusEl.classList.add('is-busy');
      } else if (event.type === 'done') {
        msg = event;
      } else if (event.type === 'error') {
        msg = { error: event.message, results: event.results || [] };
      }
    });
  } catch (e) {
    msg = { error: e.message || e, results: [] };
  }
  convertBtn.disabled = false;
  updateConvertBtn();
  if (!msg) { msg = { error: 'No response.', results: [] }; }
  if (msg.error) { statusEl.classList.remove('is-busy'); statusEl.textContent = 'Error: ' + msg.error; return; }
  const results = msg.results || [];
  const ok = results.filter(r => r.ok).length;
  statusEl.classList.remove('is-busy');
  statusEl.textContent = ok + ' of ' + results.length + ' network(s) converted.';
  if (!results.length) { resultsEl.innerHTML = ''; return; }
  resultsEl.innerHTML = '<table class="data-table"><thead><tr><th style="width:120px;">Network</th><th style="width:90px;">Status</th><th>Output Path</th><th>Error</th></tr></thead><tbody></tbody></table>';
  const tbody = resultsEl.querySelector('tbody');
  for (const r of results) {
    const row = document.createElement('tr');
    row.innerHTML = '<td class="mono">' + r.network + '</td>' +
      '<td>' + (r.ok ? '<span class="badge ok">OK</span>' : '<span class="badge err">Error</span>') + '</td>' +
      '<td class="mono wrap">' + (r.ok ? outputDisplayName(r.file) : '<span class="muted">-</span>') + '</td>' +
      '<td class="err wrap">' + (r.ok ? '' : (r.error || 'Unknown error')) + '</td>';
    tbody.appendChild(row);
  }
  if (ok > 0) { outputDir = outEl.value; openOutBtn.disabled = false; }
});
</script>
`
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	_, _ = w.Write([]byte(Page("/playworks", "Playworks Converter", body)))
}

func PlayworksPickSource(w http.ResponseWriter, r *http.Request) {
	picked := PickFiles("Select Playworks HTML", []FileFilter{{Name: "HTML", Extensions: []string{"html", "htm"}}}, false, "")
	if len(picked) == 0 {
		writeJSON(w, map[string]any{})
		return
	}
	writeJSON(w, map[string]any{"path": picked[0], "dir": filepath.Dir(picked[0]), "baseName": playworksSourceBaseName(picked[0])})
}

func PlayworksPickOutput(w http.ResponseWriter, r *http.Request) {
	writeJSON(w, map[string]any{"path": PickFolder("Select Output Folder", "")})
}

func PlayworksConvert(w http.ResponseWriter, r *http.Request) {
	send := newJSONStream(w)
	var opts playworksConvertOptions
	if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
		send(map[string]any{"type": "error", "message": err.Error(), "results": []playworksNetworkResult{}})
		return
	}
	if !fileExists(opts.SourcePath) {
		send(map[string]any{"type": "error", "message": "Source file not found.", "results": []playworksNetworkResult{}})
		return
	}
	if err := os.MkdirAll(opts.OutputDir, 0755); err != nil {
		send(map[string]any{"type": "error", "message": "Could not create output folder.", "results": []playworksNetworkResult{}})
		return
	}
	send(map[string]any{"type": "status", "message": "Reading source HTML..."})
	data, err := os.ReadFile(opts.SourcePath)
	if err != nil {
		send(map[string]any{"type": "error", "message": "Could not read source: " + err.Error(), "results": []playworksNetworkResult{}})
		return
	}

	results := []playworksNetworkResult{}
	send(map[string]any{"type": "status", "message": "Converting Unity..."})
	unityPath, err := copyPlayworksUnityInput(data, opts)
	if err != nil {
		results = append(results, playworksNetworkResult{Network: "un", OK: false, Error: err.Error()})
	} else {
		results = append(results, playworksNetworkResult{Network: "un", File: unityPath, OK: true})
	}
	for i, network := range opts.Networks {
		send(map[string]any{"type": "status", "message": fmt.Sprintf("Converting %d/%d %s", i+1, len(opts.Networks), network)})
		filePath, err := convertPlayworksNetwork(string(data), opts, network)
		if err != nil {
			results = append(results, playworksNetworkResult{Network: network, OK: false, Error: err.Error()})
			continue
		}
		results = append(results, playworksNetworkResult{Network: network, File: filePath, OK: true})
	}
	send(map[string]any{"type": "done", "results": results})
}

var playworksSuffixRx = regexp.MustCompile(`_(?:un|al|is|fb|gg|ap|mo|vu|mtg|tt)$`)
var playworksMraidScriptRx = regexp.MustCompile(`(?i)<script\b[^>]*\bsrc\s*=\s*["']mraid\.js["'][^>]*>\s*</script>\s*`)
var playworksExitapiScriptRx = regexp.MustCompile(`(?i)<script\b[^>]*\bsrc\s*=\s*["']exitapi\.js["'][^>]*>\s*</script>\s*`)
var playworksHeadCloseRx = regexp.MustCompile(`(?i)</head>`)
var playworksBodyOpenRx = regexp.MustCompile(`(?i)<body\b[^>]*>`)
var playworksBodyTagRx = regexp.MustCompile(`(?i)<body\b([^>]*)>`)
var playworksHtmlCloseRx = regexp.MustCompile(`(?i)</html\s*>`)
var playworksRemoteDebugScriptRx = regexp.MustCompile(`(?is)<script\b[^>]*>.*?insertYourRemoteDebuggingTokenHere.*?</script>\s*`)
var playworksStatsURLRx = regexp.MustCompile(`(?i)https://mrdoob\.github\.io/stats\.js/build/stats\.min\.js`)
var playworksCrossoriginRx = regexp.MustCompile(`(?i)\s+crossorigin(?:\s*=\s*("[^"]*"|'[^']*'|[^\s>]+))?`)
var playworksTypeModuleRx = regexp.MustCompile(`(?i)\s+type\s*=\s*["']module["']`)
var playworksConsoleErrorRx = regexp.MustCompile(`\bconsole\.error\s*\(`)

func playworksSourceBaseName(sourcePath string) string {
	base := strings.TrimSuffix(filepath.Base(sourcePath), filepath.Ext(sourcePath))
	return playworksSuffixRx.ReplaceAllString(base, "")
}

func playworksOutputBaseName(opts playworksConvertOptions) string {
	baseName := strings.TrimSpace(opts.BaseName)
	if baseName == "" {
		baseName = playworksSourceBaseName(opts.SourcePath)
	}
	baseName = strings.TrimSuffix(baseName, filepath.Ext(baseName))
	baseName = filepath.Base(baseName)
	baseName = strings.Map(func(r rune) rune {
		switch r {
		case '<', '>', ':', '"', '/', '\\', '|', '?', '*':
			return '_'
		default:
			return r
		}
	}, baseName)
	baseName = strings.TrimSpace(baseName)
	if baseName == "" {
		return playworksSourceBaseName(opts.SourcePath)
	}
	return baseName
}

func convertPlayworksNetwork(htmlSrc string, opts playworksConvertOptions, network string) (string, error) {
	if !playworksIsSupportedNetwork(network) {
		return "", fmt.Errorf("unsupported Playworks target network: %s", network)
	}
	transformed := transformPlayworksHTML(htmlSrc, network)
	baseName := playworksOutputBaseName(opts)
	htmlFileName := fmt.Sprintf("%s_%s.html", baseName, network)
	outputDir := filepath.Join(opts.OutputDir, playworksNetworkOutputFolder(network))
	if err := os.MkdirAll(outputDir, 0755); err != nil {
		return "", err
	}

	if playworksNeedsZip(network) {
		zipPath := filepath.Join(outputDir, fmt.Sprintf("%s_%s.zip", baseName, network))
		if err := createSingleFileZip([]byte(transformed), "index.html", zipPath); err != nil {
			return "", err
		}
		return zipPath, nil
	}

	outPath := filepath.Join(outputDir, htmlFileName)
	if err := os.WriteFile(outPath, []byte(transformed), 0644); err != nil {
		return "", err
	}
	return outPath, nil
}

func copyPlayworksUnityInput(data []byte, opts playworksConvertOptions) (string, error) {
	baseName := playworksOutputBaseName(opts)
	outputDir := filepath.Join(opts.OutputDir, "Unity")
	if err := os.MkdirAll(outputDir, 0755); err != nil {
		return "", err
	}
	outPath := filepath.Join(outputDir, fmt.Sprintf("%s_un.html", baseName))
	if err := os.WriteFile(outPath, data, 0644); err != nil {
		return "", err
	}
	return outPath, nil
}

func playworksNetworkOutputFolder(network string) string {
	switch network {
	case "al":
		return "Applovin"
	case "is":
		return "Ironsource"
	case "fb":
		return "Facebook"
	case "gg":
		return "GoogleAds"
	case "ap":
		return "Appreciate"
	case "mo":
		return "Moloco"
	case "vu":
		return "Vungle"
	case "mtg":
		return "Mintegral"
	case "tt":
		return "TikTok"
	default:
		return network
	}
}

func playworksNeedsZip(network string) bool {
	return network == "gg" || network == "ap" || network == "vu" || network == "mtg"
}

func playworksIsMraidNetwork(network string) bool {
	return network == "al" || network == "is"
}

func playworksIsExitapiNetwork(network string) bool {
	return network == "gg" || network == "ap"
}

func playworksIsSupportedNetwork(network string) bool {
	switch network {
	case "al", "is", "fb", "gg", "ap", "mo", "vu", "mtg", "tt":
		return true
	default:
		return false
	}
}

func transformPlayworksHTML(htmlSrc, network string) string {
	result := sanitizePlayworksHTML(htmlSrc)
	headInject := buildPlayworksLifecycleScript()
	if playworksIsMraidNetwork(network) {
		headInject += `<script src="mraid.js"></script>` + buildPlayworksMraidComplianceScript()
	} else if playworksIsExitapiNetwork(network) {
		headInject += `<script src="exitapi.js"></script>`
	}
	result = injectPlayworksBeforeHeadClose(result, headInject)

	if network == "vu" {
		result = injectPlayworksAfterBodyOpen(result, `<script>window.__VUNGLE__=true;</script>`)
	} else if network == "mtg" {
		result = addPlayworksBodyOnload(result, "gameReady()")
	} else if network == "tt" {
		result = injectPlayworksAfterBodyOpen(result, `<script>window.__TIKTOK__=true;</script>`)
	}

	result = replacePlayworksCTAScript(result, network)
	return finalizePlayworksNetworkHTML(result, network)
}

func sanitizePlayworksHTML(htmlSrc string) string {
	return removePlayworksNetworkScripts(cleanupPlayworksHTML(htmlSrc))
}

func cleanupPlayworksHTML(htmlSrc string) string {
	result := trimPlayworksAfterFirstHTMLClose(htmlSrc)
	result = playworksRemoteDebugScriptRx.ReplaceAllString(result, "")
	result = playworksStatsURLRx.ReplaceAllString(result, "data:text/javascript,")
	result = playworksCrossoriginRx.ReplaceAllString(result, "")
	result = playworksTypeModuleRx.ReplaceAllString(result, "")
	result = playworksConsoleErrorRx.ReplaceAllString(result, "console.warn(")
	return result
}

func trimPlayworksAfterFirstHTMLClose(htmlSrc string) string {
	loc := playworksHtmlCloseRx.FindStringIndex(htmlSrc)
	if loc == nil {
		return htmlSrc
	}
	return htmlSrc[:loc[1]]
}

func finalizePlayworksNetworkHTML(htmlSrc, network string) string {
	result := cleanupPlayworksHTML(htmlSrc)
	result = dedupePlayworksScriptSrc(result, "mraid.js", playworksIsMraidNetwork(network))
	result = dedupePlayworksScriptSrc(result, "exitapi.js", playworksIsExitapiNetwork(network))
	return result
}

func removePlayworksNetworkScripts(htmlSrc string) string {
	result := playworksMraidScriptRx.ReplaceAllString(htmlSrc, "")
	result = playworksExitapiScriptRx.ReplaceAllString(result, "")
	return result
}

func dedupePlayworksScriptSrc(htmlSrc, src string, shouldExist bool) string {
	rx := regexp.MustCompile(`(?i)<script\b[^>]*\bsrc\s*=\s*["']` + regexp.QuoteMeta(src) + `["'][^>]*>\s*</script>\s*`)
	seen := false
	result := rx.ReplaceAllStringFunc(htmlSrc, func(match string) string {
		if shouldExist && !seen {
			seen = true
			return `<script src="` + src + `"></script>`
		}
		return ""
	})
	if shouldExist && !seen {
		result = injectPlayworksBeforeHeadClose(result, `<script src="`+src+`"></script>`)
	}
	return result
}

func injectPlayworksBeforeHeadClose(htmlSrc, injection string) string {
	loc := playworksHeadCloseRx.FindStringIndex(htmlSrc)
	if loc != nil {
		return htmlSrc[:loc[0]] + injection + "\n" + htmlSrc[loc[0]:]
	}
	return injection + "\n" + htmlSrc
}

func injectPlayworksAfterBodyOpen(htmlSrc, injection string) string {
	loc := playworksBodyOpenRx.FindStringIndex(htmlSrc)
	if loc == nil {
		return injection + "\n" + htmlSrc
	}
	return htmlSrc[:loc[1]] + "\n" + injection + htmlSrc[loc[1]:]
}

func addPlayworksBodyOnload(htmlSrc, handler string) string {
	loc := playworksBodyTagRx.FindStringSubmatchIndex(htmlSrc)
	if loc == nil {
		return `<body onload="` + handler + `">` + "\n" + htmlSrc
	}
	attrs := htmlSrc[loc[2]:loc[3]]
	onloadStart, onloadEnd, quote, valueStart, valueEnd := findPlayworksOnloadAttr(attrs)
	if onloadStart == -1 {
		updated := `<body` + attrs + ` onload="` + handler + `">`
		return htmlSrc[:loc[0]] + updated + htmlSrc[loc[1]:]
	}
	value := attrs[valueStart:valueEnd]
	if strings.Contains(value, handler) {
		return htmlSrc
	}
	replacement := `onload=` + quote + value + `;` + handler + quote
	updatedAttrs := attrs[:onloadStart] + replacement + attrs[onloadEnd:]
	updatedTag := `<body` + updatedAttrs + `>`
	return htmlSrc[:loc[0]] + updatedTag + htmlSrc[loc[1]:]
}

func findPlayworksOnloadAttr(attrs string) (start int, end int, quote string, valueStart int, valueEnd int) {
	lower := strings.ToLower(attrs)
	idx := strings.Index(lower, "onload")
	for idx != -1 {
		if idx > 0 {
			prev := lower[idx-1]
			if (prev >= 'a' && prev <= 'z') || (prev >= '0' && prev <= '9') || prev == '-' || prev == '_' {
				next := strings.Index(lower[idx+len("onload"):], "onload")
				if next == -1 {
					return -1, -1, "", -1, -1
				}
				idx += len("onload") + next
				continue
			}
		}
		pos := idx + len("onload")
		for pos < len(attrs) && (attrs[pos] == ' ' || attrs[pos] == '\t' || attrs[pos] == '\n' || attrs[pos] == '\r') {
			pos++
		}
		if pos >= len(attrs) || attrs[pos] != '=' {
			return -1, -1, "", -1, -1
		}
		pos++
		for pos < len(attrs) && (attrs[pos] == ' ' || attrs[pos] == '\t' || attrs[pos] == '\n' || attrs[pos] == '\r') {
			pos++
		}
		if pos >= len(attrs) || (attrs[pos] != '"' && attrs[pos] != '\'') {
			return -1, -1, "", -1, -1
		}
		q := attrs[pos]
		valueStart = pos + 1
		valueEnd = strings.IndexByte(attrs[valueStart:], q)
		if valueEnd == -1 {
			return -1, -1, "", -1, -1
		}
		valueEnd += valueStart
		return idx, valueEnd + 1, string(q), valueStart, valueEnd
	}
	return -1, -1, "", -1, -1
}

func replacePlayworksCTAScript(htmlSrc, network string) string {
	marker := "Luna.Unity.Playable.InstallFullGame=function"
	markerIdx := strings.LastIndex(htmlSrc, marker)
	if markerIdx == -1 {
		return htmlSrc
	}
	scriptOpenIdx := strings.LastIndex(htmlSrc[:markerIdx], "<script>")
	if scriptOpenIdx == -1 {
		return htmlSrc
	}
	scriptCloseRel := strings.Index(htmlSrc[markerIdx:], "</script>")
	if scriptCloseRel == -1 {
		return htmlSrc
	}
	scriptCloseIdx := markerIdx + scriptCloseRel
	return htmlSrc[:scriptOpenIdx] + buildPlayworksConsoleRestoreScript() + buildPlayworksCTAScript(network) + htmlSrc[scriptCloseIdx+len("</script>"):]
}

func buildPlayworksConsoleRestoreScript() string {
	return strings.Join([]string{
		`<script>(function(){`,
		`try{`,
		`var f=document.createElement("iframe");`,
		`f.style.display="none";`,
		`document.documentElement.appendChild(f);`,
		`var nc=f.contentWindow.console;`,
		`document.documentElement.removeChild(f);`,
		`var methods=["log","warn","error","info","debug","dir","table","group","groupEnd","groupCollapsed","time","timeEnd","assert","count","countReset","trace"];`,
		`methods.forEach(function(m){try{if(typeof nc[m]==="function")window.console[m]=nc[m].bind(nc);}catch(e){}});`,
		`}catch(e){}`,
		`})();</script>`,
	}, "")
}

func buildPlayworksLifecycleScript() string {
	return strings.Join([]string{
		`<script>(function(){`,
		`if(typeof window.gameReady!=="function")window.gameReady=function(){};`,
		`if(typeof window.gameStart!=="function")window.gameStart=function(){};`,
		`if(typeof window.gameEnd!=="function")window.gameEnd=function(){};`,
		`if(typeof window.gameClose!=="function")window.gameClose=function(){};`,
		`var _grOnce=false,_gsOnce=false;`,
		`window.addEventListener("luna:build",function(){if(_grOnce)return;_grOnce=true;try{window.gameReady();}catch(e){}});`,
		`window.addEventListener("luna:start",function(){if(_gsOnce)return;_gsOnce=true;try{window.gameStart();}catch(e){}});`,
		`window.addEventListener("luna:ended",function(){try{window.gameEnd();}catch(e){}});`,
		`})();</script>`,
	}, "")
}

func buildPlayworksMraidComplianceScript() string {
	return strings.Join([]string{
		`<script>(function(){`,
		`var m=null,scene=null,viewable=true,exposed=true,volume=null;`,
		`function get(){return window.mraid||m;}`,
		`function emit(name,detail){try{window.dispatchEvent(new CustomEvent(name,{detail:detail}));}catch(e){}}`,
		`function apply(){emit("hpl:mraid:visibility",{viewable:viewable,exposed:exposed,hidden:!viewable||!exposed});if(scene&&scene.sound&&typeof scene.sound.setMute==="function")scene.sound.setMute(!viewable||!exposed);}`,
		`function onViewable(v){viewable=!!v;apply();}`,
		`function onExposure(p){if(typeof p==="number")exposed=p>0;apply();}`,
		`function onVolume(v){if(typeof v==="number"){volume=v/100;emit("hpl:mraid:volume",volume);if(scene&&scene.sound&&typeof scene.sound.setVolume==="function")scene.sound.setVolume(volume);}}`,
		`function add(name,fn){var x=get();try{if(x&&typeof x.addEventListener==="function")x.addEventListener(name,fn);}catch(e){}}`,
		`function unload(){try{if(typeof mraid!=="undefined"&&typeof mraid.unload==="function")mraid.unload();}catch(e){}}`,
		`window.__hplMraidUnload=unload;`,
		`function setup(){var x=get();if(!x||setup.done)return;setup.done=true;m=x;try{if(typeof mraid!=="undefined"&&typeof mraid.isViewable==="function")viewable=!!mraid.isViewable();else if(typeof x.isViewable==="function")viewable=!!x.isViewable();}catch(e){}try{if(typeof mraid!=="undefined"&&typeof mraid.addEventListener==="function"){mraid.addEventListener("error",function(message,action){console.warn("[MRAID error]",{message:message,action:action});});mraid.addEventListener("stateChange",function(state){console.log("[MRAID stateChange]",state);});mraid.addEventListener("exposureChange",onExposure);mraid.addEventListener("viewableChange",onViewable);mraid.addEventListener("audioVolumeChange",onVolume);apply();return;}}catch(e){}add("error",function(message,action){console.warn("[MRAID error]",{message:message,action:action});});add("stateChange",function(state){console.log("[MRAID stateChange]",state);});add("exposureChange",onExposure);add("viewableChange",onViewable);add("audioVolumeChange",onVolume);apply();}`,
		`function ready(){var x=get();if(!x)return false;try{if(typeof mraid!=="undefined"&&typeof mraid.getState==="function"&&mraid.getState()==="loading"){mraid.addEventListener("ready",setup);return true;}if(typeof x.getState==="function"&&x.getState()==="loading"){add("ready",setup);return true;}}catch(e){}setup();return true;}`,
		`window.__hplMraidBindScene=function(s){scene=s;apply();if(typeof volume==="number"&&scene&&scene.sound&&typeof scene.sound.setVolume==="function")scene.sound.setVolume(volume);};`,
		`if(!ready()){var end=Date.now()+500;(function poll(){if(ready()||Date.now()>end)return;setTimeout(poll,50);})();}`,
		`setTimeout(setup,2000);`,
		`})();</script>`,
	}, "")
}

func buildPlayworksCTAScript(network string) string {
	urlSetup := `n=n||window.$environment.packageConfig.iosLink,i=i||window.$environment.packageConfig.androidLink;var o=/iphone|ipad|ipod|macintosh/i.test(window.navigator.userAgent.toLowerCase())?n:i;`
	closeCall := `try{window.gameClose();}catch(e){}`
	ctaLogic := closeCall + `window.open(o,"_blank");`
	switch network {
	case "al", "is":
		ctaLogic = closeCall + `if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){var s=typeof mraid.getState==="function"?mraid.getState():"default";if(s!=="loading"){mraid.open(o);return;}}window.open(o,"_blank");`
	case "fb":
		ctaLogic = closeCall + `if(typeof FbPlayableAd!=="undefined"&&typeof FbPlayableAd.onCTAClick==="function"){FbPlayableAd.onCTAClick();return;}window.open(o,"_blank");`
	case "gg", "ap":
		ctaLogic = closeCall + `if(typeof ExitApi!=="undefined"&&typeof ExitApi.exit==="function"){ExitApi.exit();return;}window.open(o,"_blank");`
	case "mo":
		ctaLogic = closeCall + `if(typeof FbPlayableAd!=="undefined"&&typeof FbPlayableAd.onCTAClick==="function"){FbPlayableAd.onCTAClick();return;}if(typeof window.clickTag==="string"&&window.clickTag){window.open(window.clickTag,"_blank");return;}window.open(o,"_blank");`
	case "vu":
		ctaLogic = closeCall + `try{parent.postMessage("download","*");}catch(e){}`
	case "mtg":
		ctaLogic = closeCall + `if(typeof window.install==="function"){window.install();return;}window.open(o,"_blank");`
	case "tt":
		ctaLogic = closeCall + `if(typeof window.openAppStore==="function"){window.openAppStore();return;}window.open(o,"_blank");`
	}
	return `<script>window.addEventListener("luna:build",(function(){Bridge.ready((function(){Luna.Unity.Playable.InstallFullGame=function(n,i){window.pi.logCta(),` + urlSetup + ctaLogic + `}}))}));</script>`
}

func createSingleFileZip(data []byte, nameInZip, destZipPath string) error {
	_ = os.Remove(destZipPath)
	out, err := os.Create(destZipPath)
	if err != nil {
		return err
	}
	defer out.Close()

	zw := zip.NewWriter(out)
	entry, err := zw.Create(nameInZip)
	if err != nil {
		_ = zw.Close()
		return err
	}
	if _, err := entry.Write(data); err != nil {
		_ = zw.Close()
		return err
	}
	return zw.Close()
}
