package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"math/rand"
	"mime/multipart"
	"net/http"
	urlpkg "net/url"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"time"
)

var applovinUserAgents = []string{
	"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
	"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
	"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0",
	"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
	"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15",
	"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0",
	"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
}

func pickApplovinUserAgent(randomize bool) string {
	if !randomize {
		return applovinUserAgents[0]
	}
	return applovinUserAgents[rand.Intn(len(applovinUserAgents))]
}

func ApplovinPage(w http.ResponseWriter, r *http.Request) {
	body := `
<header class="tool-header">
  <h2 class="tool-title">AppLovin Playable Preview</h2>
  <p class="tool-description">Upload HTML files to p.applov.in and get QR codes to scan with the AppLovin Playable Preview app.</p>
</header>

<section class="tool-panel">
  <div class="panel-header"><h3 class="panel-title">Inputs</h3></div>
  <div class="panel-body">
    <div class="control-group">
      <div id="dropZone" class="drop-zone">
        <div class="file-row">
          <button id="pickFolder" class="secondary">Select Folder</button>
          <button id="pick" class="secondary">Select File(s)</button>
          <button id="clear" class="secondary">Clear</button>
        </div>
        <div class="drop-zone-text">or drop HTML files or folders here</div>
      </div>
      <div id="fileName" class="file-name" style="margin-top:8px;">(no files selected)</div>
      <div id="fileList" class="selected-files"></div>
    </div>
    <div class="action-row">
      <button id="upload">Upload</button>
      <button id="saveAll" class="secondary" disabled>Save All</button>
      <button id="regenAll" class="secondary" disabled>Regenerate</button>
    </div>
    <div id="status" class="status-panel"></div>
  </div>
</section>

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

<style>
  .qr-cell { text-align:center; }
  .qr-cell img { width:104px; height:104px; max-width:100%; padding:6px; border-radius:4px; background:#fff; cursor:zoom-in; }
  .result-actions { display:flex; gap:6px; flex-wrap:wrap; }
  .qr-results:not(.has-errors) .error-col { display:none; }
  .qr-modal { position:fixed; inset:0; display:none; align-items:center; justify-content:center; padding:24px; background:rgba(0,0,0,0.72); z-index:10; }
  .qr-modal.open { display:flex; }
  .qr-modal img { width:min(70vw,520px); height:min(70vw,520px); padding:18px; border-radius:4px; background:#fff; }
</style>
<div id="qrModal" class="qr-modal"><img alt="Expanded QR" /></div>

<script>
const PAGE_SIZE = 5;
let selectedPage = 0;
const pickBtn = document.getElementById('pick');
const pickFolderBtn = document.getElementById('pickFolder');
const clearBtn = document.getElementById('clear');
const uploadBtn = document.getElementById('upload');
const fileNameEl = document.getElementById('fileName');
const fileListEl = document.getElementById('fileList');
const statusEl = document.getElementById('status');
const resultsEl = document.getElementById('results');
const saveAllBtn = document.getElementById('saveAll');
const regenAllBtn = document.getElementById('regenAll');
const qrModal = document.getElementById('qrModal');
let selectedPaths = [];
let resultItems = [];

function basename(p) {
  const i = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\'));
  return i >= 0 ? p.slice(i + 1) : p;
}
function renderSelection() {
  const oldPager = document.getElementById('selectedPager');
  if (oldPager) oldPager.remove();
  if (!selectedPaths.length) {
    fileNameEl.textContent = '(no files selected)';
    fileListEl.innerHTML = '';
    return;
  }
  fileNameEl.textContent = selectedPaths.length + ' file' + (selectedPaths.length === 1 ? '' : 's');
  const maxPage = Math.max(0, Math.ceil(selectedPaths.length / PAGE_SIZE) - 1);
  selectedPage = Math.min(selectedPage, maxPage);
  const start = selectedPage * PAGE_SIZE;
  const visiblePaths = selectedPaths.slice(start, start + PAGE_SIZE);
  fileListEl.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>' +
    visiblePaths.map((p, offset) => {
      const i = start + offset;
      return '<tr><td class="mono wrap">' + escapeHtml(basename(p)) + '</td><td><button class="remove-selected danger" data-index="' + i + '" title="Remove">×</button></td></tr>';
    }).join('') + '</tbody></table></div>';
  fileListEl.querySelectorAll('.remove-selected').forEach(btn => {
    btn.addEventListener('click', () => { selectedPaths.splice(Number(btn.dataset.index), 1); renderSelection(); });
  });
  if (selectedPaths.length > PAGE_SIZE) {
    const pager = document.createElement('div');
    pager.id = 'selectedPager';
    pager.className = 'pager';
    pager.innerHTML = '<button class="secondary" id="prevPage"' + (selectedPage === 0 ? ' disabled' : '') + '>Previous</button>' +
      '<span class="file-name">Page ' + (selectedPage + 1) + ' of ' + (maxPage + 1) + '</span>' +
      '<button class="secondary" id="nextPage"' + (selectedPage === maxPage ? ' disabled' : '') + '>Next</button>';
    fileListEl.appendChild(pager);
    pager.querySelector('#prevPage').addEventListener('click', () => { selectedPage--; renderSelection(); });
    pager.querySelector('#nextPage').addEventListener('click', () => { selectedPage++; renderSelection(); });
  }
}

saveAllBtn.addEventListener('click', async () => {
  if (!resultItems.length) return;
  saveAllBtn.disabled = true;
  try {
    const r = await fetch('/api/applovin/saveAllQrs', {
      method: 'POST',
      headers: {'Content-Type':'application/json'},
      body: JSON.stringify({ items: resultItems }),
    });
    const j = await r.json();
    statusEl.innerHTML = j.ok
      ? '<span class="ok">' + escapeHtml(j.message || 'Saved.') + '</span>'
      : '<span class="err">' + escapeHtml(j.message || 'Save failed.') + '</span>';
  } finally {
    saveAllBtn.disabled = false;
  }
});

regenAllBtn.addEventListener('click', () => {
  resultsEl.querySelectorAll('img[data-qr]').forEach(img => {
    const base = img.getAttribute('data-qr');
    img.src = base + '&_=' + Date.now();
  });
});
setupDropZone('dropZone', statusEl, (j) => {
  if (j.files && j.files.length) {
    const added = j.files.map(f => f.path).filter(p => !selectedPaths.includes(p));
    selectedPaths = selectedPaths.concat(added);
    selectedPage = Math.max(0, Math.ceil(selectedPaths.length / PAGE_SIZE) - 1);
    renderSelection();
  }
  if (j.skipped) statusEl.textContent = 'Skipped ' + j.skipped + ' non-HTML or unavailable item(s).';
});

clearBtn.addEventListener('click', () => {
  selectedPaths = [];
  selectedPage = 0;
  renderSelection();
});

pickFolderBtn.addEventListener('click', async () => {
  const r = await fetch('/api/applovin/pickFolder', { method: 'POST' });
  const j = await r.json();
  if (j.files && j.files.length) {
    const added = j.files.map(f => f.path).filter(p => !selectedPaths.includes(p));
    selectedPaths = selectedPaths.concat(added);
    selectedPage = Math.max(0, Math.ceil(selectedPaths.length / PAGE_SIZE) - 1);
    renderSelection();
  }
});
pickBtn.addEventListener('click', async () => {
  const r = await fetch('/api/applovin/pick', { method: 'POST' });
  const j = await r.json();
  if (j.files && j.files.length) {
    const added = j.files.map(f => f.path).filter(p => !selectedPaths.includes(p));
    selectedPaths = selectedPaths.concat(added);
    selectedPage = Math.max(0, Math.ceil(selectedPaths.length / PAGE_SIZE) - 1);
    renderSelection();
  }
});

uploadBtn.addEventListener('click', async () => {
  statusEl.textContent = '';
  statusEl.classList.remove('is-busy');
  resultsEl.innerHTML = '';
  if (!selectedPaths.length) {
    statusEl.innerHTML = '<span class="err">Pick at least one HTML file first.</span>';
    return;
  }
  uploadBtn.disabled = true;
  statusEl.textContent = 'Uploading...';
  statusEl.classList.add('is-busy');
  resultItems = [];
  saveAllBtn.disabled = true;
  regenAllBtn.disabled = true;
  try {
    const res = await fetch('/api/applovin/upload', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ paths: selectedPaths }),
    });
    if (!res.ok || !res.body) {
      statusEl.classList.remove('is-busy');
      statusEl.innerHTML = '<span class="err">Upload failed.</span>';
      uploadBtn.disabled = false;
      return;
    }
    const reader = res.body.getReader();
    const dec = new TextDecoder();
    let buf = '';
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      buf += dec.decode(value, { stream: true });
      let nl;
      while ((nl = buf.indexOf('\n')) >= 0) {
        const line = buf.slice(0, nl); buf = buf.slice(nl + 1);
        if (!line.trim()) continue;
        handle(JSON.parse(line));
      }
    }
  } catch (e) {
    statusEl.classList.remove('is-busy');
    statusEl.innerHTML = '<span class="err">' + (e.message || e) + '</span>';
  } finally {
    uploadBtn.disabled = false;
  }
});

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

function ensureResultsTable() {
  let tbody = resultsEl.querySelector('tbody');
  if (tbody) return tbody;
  resultsEl.innerHTML = '<table class="data-table qr-results"><thead><tr><th>File</th><th style="width:140px;">QR</th><th style="width:170px;">Actions</th><th class="error-col">Error</th></tr></thead><tbody></tbody></table>';
  return resultsEl.querySelector('tbody');
}
qrModal.addEventListener('click', () => qrModal.classList.remove('open'));

function handle(m) {
  if (m.type === 'status') {
    statusEl.textContent = m.message;
    statusEl.classList.add('is-busy');
  } else if (m.type === 'fileError') {
    const tbody = ensureResultsTable();
    resultsEl.querySelector('table').classList.add('has-errors');
    const row = document.createElement('tr');
    row.innerHTML = '<td class="mono wrap">' + escapeHtml(m.name) + '</td><td class="muted">-</td><td class="muted">-</td><td class="err wrap error-col">' + escapeHtml(m.message) + '</td>';
    tbody.appendChild(row);
  } else if (m.type === 'fileResult') {
    const tbody = ensureResultsTable();
    const row = document.createElement('tr');
    const file = document.createElement('td');
    file.className = 'mono wrap';
    file.textContent = m.name;
    const qr = document.createElement('td');
    qr.className = 'qr-cell';
    qr.innerHTML = '<img src="' + m.qrImg + '" data-qr="' + m.qrImg + '" alt="QR" />';
    qr.querySelector('img').addEventListener('click', (event) => {
      qrModal.querySelector('img').src = event.currentTarget.src;
      qrModal.classList.add('open');
    });
    const actionCell = document.createElement('td');
    const error = document.createElement('td');
    error.className = 'error-col';
    error.innerHTML = '<span class="muted">-</span>';
    const actions = document.createElement('div');
    actions.className = 'result-actions';
    const openBtn = document.createElement('button');
    openBtn.textContent = 'Open';
    openBtn.className = 'secondary';
    openBtn.onclick = () => fetch('/api/open', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:m.previewUrl})});
    const saveQr = document.createElement('button');
    saveQr.textContent = 'Save QR';
    saveQr.className = 'secondary';
    saveQr.onclick = async () => {
      saveQr.disabled = true;
      try {
        const r = await fetch('/api/applovin/saveQr', {
          method: 'POST',
          headers: {'Content-Type':'application/json'},
          body: JSON.stringify({ name: m.name, url: m.qrImg }),
        });
        const j = await r.json();
        statusEl.innerHTML = j.ok
          ? '<span class="ok">' + escapeHtml(j.message || 'Saved.') + '</span>'
          : '<span class="err">' + escapeHtml(j.message || 'Save failed.') + '</span>';
      } finally {
        saveQr.disabled = false;
      }
    };
    actions.appendChild(openBtn); actions.appendChild(saveQr);
    actionCell.appendChild(actions);
    row.append(file, qr, actionCell, error);
    tbody.appendChild(row);
    resultItems.push({ name: m.name, url: m.qrImg });
    saveAllBtn.disabled = resultItems.length === 0;
    regenAllBtn.disabled = resultItems.length === 0;
  } else if (m.type === 'done') {
    statusEl.classList.remove('is-busy');
    statusEl.innerHTML = '<span class="ok">Done.</span>';
  }
}
</script>
`
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	_, _ = w.Write([]byte(Page("/applovin", "AppLovin Playable Preview", body)))
}

func ApplovinPick(w http.ResponseWriter, r *http.Request) {
	cfg := LoadConfig()
	picked := PickFiles(
		"Select HTML files",
		[]FileFilter{{Name: "HTML", Extensions: []string{"html", "htm"}}},
		true, cfg.LastPickDir,
	)
	if len(picked) > 0 {
		cfg.LastPickDir = filepath.Dir(picked[0])
		_ = SaveConfig(cfg)
	}
	files := make([]map[string]string, 0, len(picked))
	for _, p := range picked {
		files = append(files, map[string]string{"path": p, "name": filepath.Base(p)})
	}
	writeJSON(w, map[string]any{"files": files})
}

func ApplovinPickFolder(w http.ResponseWriter, r *http.Request) {
	cfg := LoadConfig()
	dir := PickFolder("Select folder", cfg.LastPickDir)
	if dir == "" {
		writeJSON(w, map[string]any{"files": []any{}})
		return
	}
	cfg.LastPickDir = dir
	_ = SaveConfig(cfg)
	paths := collectHTMLFiles(dir)
	files := make([]map[string]string, 0, len(paths))
	for _, p := range paths {
		files = append(files, map[string]string{"path": p, "name": filepath.Base(p)})
	}
	writeJSON(w, map[string]any{"files": files})
}

func ApplovinUpload(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Paths []string `json:"paths"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	cfg := LoadConfig().Applovin
	cookie := cfg.Cookie
	delayMs := cfg.DelayMs
	if delayMs < 0 {
		delayMs = 0
	}
	randomizeUA := true
	if cfg.RandomizeUserAgent != nil {
		randomizeUA = *cfg.RandomizeUserAgent
	}

	w.Header().Set("Content-Type", "application/x-ndjson")
	flusher, _ := w.(http.Flusher)

	send := func(obj any) {
		data, _ := json.Marshal(obj)
		_, _ = w.Write(append(data, '\n'))
		if flusher != nil {
			flusher.Flush()
		}
	}

	htmlRx := regexp.MustCompile(`(?i)\.html?$`)

	buildForm := func(filePath string) (*bytes.Buffer, string, error) {
		data, err := os.ReadFile(filePath)
		if err != nil {
			return nil, "", err
		}
		var buf bytes.Buffer
		mw := multipart.NewWriter(&buf)
		fw, err := createFormFileWithType(mw, "file", filepath.Base(filePath), "text/html")
		if err != nil {
			return nil, "", err
		}
		if _, err := fw.Write(data); err != nil {
			return nil, "", err
		}
		if err := mw.Close(); err != nil {
			return nil, "", err
		}
		return &buf, mw.FormDataContentType(), nil
	}

	doPost := func(url string, body *bytes.Buffer, contentType string) (int, string, error) {
		req, err := http.NewRequest("POST", url, body)
		if err != nil {
			return 0, "", err
		}
		req.Header.Set("Content-Type", contentType)
		req.Header.Set("Accept", "application/json, text/javascript, */*; q=0.01")
		req.Header.Set("Origin", "https://p.applov.in")
		req.Header.Set("Referer", "https://p.applov.in/playablePreview?create=1&qr=1")
		req.Header.Set("X-Requested-With", "XMLHttpRequest")
		req.Header.Set("User-Agent", pickApplovinUserAgent(randomizeUA))
		if cookie != "" {
			req.Header.Set("Cookie", "__Host-APPLOVINID="+cookie)
		}
		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			return 0, "", err
		}
		defer resp.Body.Close()
		out, _ := io.ReadAll(resp.Body)
		return resp.StatusCode, string(out), nil
	}

	for i, filePath := range req.Paths {
		fileName := filepath.Base(filePath)
		idx := i + 1
		fileErr := func(msg string) {
			send(map[string]any{"type": "fileError", "name": fileName, "message": msg})
		}

		if filePath == "" || !fileExists(filePath) {
			fileErr("File missing")
			continue
		}
		if !htmlRx.MatchString(filePath) {
			fileErr("Must be .html")
			continue
		}

		if i > 0 && delayMs > 0 {
			send(map[string]any{"type": "status", "message": fmt.Sprintf("Waiting %d/%d %s", idx, len(req.Paths), fileName)})
			time.Sleep(time.Duration(delayMs) * time.Millisecond)
		}

		send(map[string]any{"type": "status", "message": fmt.Sprintf("Validating %d/%d %s", idx, len(req.Paths), fileName)})

		body, ct, err := buildForm(filePath)
		if err != nil {
			fileErr("Read failed: " + err.Error())
			continue
		}
		status, respText, err := doPost("https://p.applov.in/validateHTMLContent", body, ct)
		if err != nil {
			fileErr("Network (validate): " + err.Error())
			continue
		}
		if status < 200 || status >= 300 {
			fileErr(fmt.Sprintf("Validate HTTP %d: %s", status, truncate(respText, 300)))
			continue
		}
		var vJSON map[string]any
		_ = json.Unmarshal([]byte(respText), &vJSON)
		if code, ok := vJSON["code"]; ok {
			if codeNum, isNum := code.(float64); isNum && codeNum != 200 {
				fileErr("Validation failed: " + truncate(respText, 300))
				continue
			}
		}

		send(map[string]any{"type": "status", "message": fmt.Sprintf("Uploading %d/%d %s", idx, len(req.Paths), fileName)})

		body, ct, err = buildForm(filePath)
		if err != nil {
			fileErr("Read failed: " + err.Error())
			continue
		}
		status, respText, err = doPost("https://p.applov.in/getCachedAdURL", body, ct)
		if err != nil {
			fileErr("Network (cache): " + err.Error())
			continue
		}
		if status < 200 || status >= 300 {
			fileErr(fmt.Sprintf("Cache HTTP %d: %s", status, truncate(respText, 300)))
			continue
		}
		var cJSON map[string]any
		if err := json.Unmarshal([]byte(respText), &cJSON); err != nil {
			fileErr("Non-JSON: " + truncate(respText, 300))
			continue
		}
		hash, _ := cJSON["results"].(string)
		if hash == "" {
			fileErr("No hash in response: " + truncate(respText, 300))
			continue
		}

		send(map[string]any{
			"type":       "fileResult",
			"name":       fileName,
			"hash":       hash,
			"previewUrl": "https://p.applov.in/getPreviewHTML?preview=" + hash,
			"pageUrl":    "https://p.applov.in/playablePreview?preview=" + hash + "&qr=1",
			"qrImg":      "https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=" + urlpkg.QueryEscape(hash),
		})
	}
	send(map[string]any{"type": "done"})
}

func qrBaseName(name string) string {
	base := name
	if base == "" {
		base = "qr"
	}
	base = regexp.MustCompile(`(?i)\.html?$`).ReplaceAllString(base, "")
	base = regexp.MustCompile(`[\\/:*?"<>|]`).ReplaceAllString(base, "_")
	if base == "" {
		base = "qr"
	}
	return base
}

func downloadQR(url, destPath string) error {
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return fmt.Errorf("HTTP %d", resp.StatusCode)
	}
	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	return os.WriteFile(destPath, data, 0644)
}

func ApplovinSaveQR(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Name string `json:"name"`
		URL  string `json:"url"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSON(w, map[string]any{"ok": false, "message": err.Error()})
		return
	}
	if req.URL == "" {
		writeJSON(w, map[string]any{"ok": false, "message": "Missing URL"})
		return
	}
	base := qrBaseName(req.Name)
	dest := PickSaveFile("Save QR", base+".png", []FileFilter{{Name: "PNG Image", Extensions: []string{"png"}}}, "")
	if dest == "" {
		writeJSON(w, map[string]any{"ok": false, "message": "Cancelled"})
		return
	}
	if err := downloadQR(req.URL, dest); err != nil {
		writeJSON(w, map[string]any{"ok": false, "message": "Save failed: " + err.Error()})
		return
	}
	writeJSON(w, map[string]any{"ok": true, "message": "Saved QR: " + dest})
}

func ApplovinSaveAllQRs(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Items []struct {
			Name string `json:"name"`
			URL  string `json:"url"`
		} `json:"items"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSON(w, map[string]any{"ok": false, "message": err.Error()})
		return
	}
	if len(req.Items) == 0 {
		writeJSON(w, map[string]any{"ok": false, "message": "No items"})
		return
	}
	dir := PickFolder("Choose folder to save QRs", "")
	if dir == "" {
		writeJSON(w, map[string]any{"ok": false, "message": "Cancelled"})
		return
	}
	saved := 0
	errs := []string{}
	for _, it := range req.Items {
		base := qrBaseName(it.Name)
		dest := filepath.Join(dir, base+".png")
		n := 1
		for fileExists(dest) {
			dest = filepath.Join(dir, fmt.Sprintf("%s (%d).png", base, n))
			n++
		}
		if err := downloadQR(it.URL, dest); err != nil {
			errs = append(errs, it.Name+": "+err.Error())
			continue
		}
		saved++
	}
	if len(errs) > 0 {
		writeJSON(w, map[string]any{
			"ok":      false,
			"message": fmt.Sprintf("Saved %d/%d. Errors: %s", saved, len(req.Items), strings.Join(errs, "; ")),
		})
		return
	}
	writeJSON(w, map[string]any{
		"ok":      true,
		"message": fmt.Sprintf("Saved %d QR%s to %s", saved, plural(saved), dir),
	})
}

func plural(n int) string {
	if n == 1 {
		return ""
	}
	return "s"
}
