package main

import (
	"encoding/json"
	"fmt"
	"io/fs"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"strings"
)

func Base64Page(w http.ResponseWriter, r *http.Request) {
	body := `
<header class="tool-header">
  <h2 class="tool-title">Base64 Scanner</h2>
  <p class="tool-description">Scan selected HTML files for asset references that are not embedded as base64 data URIs.</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="pickFiles" 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="selection" class="selected-list"></div>
    </div>
    <div class="action-row"><button id="scan">Scan</button></div>
    <div id="status" class="status-panel"></div>
  </div>
</section>
<div id="results" class="results-panel"></div>

<style>
  .asset-list { margin:0; padding-left:16px; }
  .asset-list li + li { margin-top:3px; }
</style>

<script>
const PAGE_SIZE = 5;
const statusEl = document.getElementById('status');
const resultsEl = document.getElementById('results');
const selectionEl = document.getElementById('selection');
let pickedFiles = [];
let selectedPage = 0;
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 renderSelection() {
  const oldPager = document.getElementById('selectedPager');
  if (oldPager) oldPager.remove();
  if (!pickedFiles.length) {
    selectionEl.innerHTML = '<span class="file-name">(no files selected)</span>';
    return;
  }
  const maxPage = Math.max(0, Math.ceil(pickedFiles.length / PAGE_SIZE) - 1);
  selectedPage = Math.min(selectedPage, maxPage);
  const start = selectedPage * PAGE_SIZE;
  const visible = pickedFiles.slice(start, start + PAGE_SIZE);
  selectionEl.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>' +
    visible.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>';
  selectionEl.querySelectorAll('.remove-selected').forEach(btn => {
    btn.addEventListener('click', () => { pickedFiles.splice(Number(btn.dataset.index), 1); renderSelection(); });
  });
  if (pickedFiles.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>';
    selectionEl.appendChild(pager);
    pager.querySelector('#prevPage').addEventListener('click', () => { selectedPage--; renderSelection(); });
    pager.querySelector('#nextPage').addEventListener('click', () => { selectedPage++; renderSelection(); });
  }
}
function addPaths(paths) {
  const existing = new Set(pickedFiles);
  pickedFiles = pickedFiles.concat((paths || []).filter(p => !existing.has(p)));
  selectedPage = Math.max(0, Math.ceil(pickedFiles.length / PAGE_SIZE) - 1);
  renderSelection();
}
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));
}
renderSelection();
setupDropZone('dropZone', statusEl, (j) => {
  addPaths(j.paths || []);
  if (j.skipped) statusEl.textContent = 'Skipped ' + j.skipped + ' non-HTML or unavailable item(s).';
});

document.getElementById('pickFolder').addEventListener('click', async () => {
  const r = await fetch('/api/base64/pickFolder', { method: 'POST' });
  const j = await r.json();
  addPaths(j.paths || []);
});
document.getElementById('pickFiles').addEventListener('click', async () => {
  const r = await fetch('/api/base64/pickFiles', { method: 'POST' });
  const j = await r.json();
  addPaths(j.paths || []);
});
document.getElementById('clear').addEventListener('click', () => {
  pickedFiles = [];
  selectedPage = 0;
  statusEl.textContent = '';
  statusEl.classList.remove('is-busy');
  resultsEl.innerHTML = '';
  renderSelection();
});
document.getElementById('scan').addEventListener('click', async () => {
  statusEl.textContent = 'Scanning...';
  statusEl.classList.add('is-busy');
  resultsEl.innerHTML = '';
  if (!pickedFiles.length) { statusEl.classList.remove('is-busy'); statusEl.textContent = 'Pick files first.'; return; }
  const payload = { files: pickedFiles };
  let j = null;
  try {
    const r = await fetch('/api/base64/scan', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
    await readJSONStream(r, (msg) => {
      if (msg.type === 'status') {
        statusEl.textContent = msg.message;
        statusEl.classList.add('is-busy');
      } else if (msg.type === 'done') {
        j = msg;
      } else if (msg.type === 'error') {
        j = { error: msg.message };
      }
    });
  } catch (e) {
    statusEl.classList.remove('is-busy');
    statusEl.textContent = 'Error: ' + (e.message || e);
    return;
  }
  if (!j) { statusEl.classList.remove('is-busy'); statusEl.textContent = 'Error: No response.'; return; }
  if (j.error) { statusEl.classList.remove('is-busy'); statusEl.textContent = 'Error: ' + j.error; return; }
  const total = j.results.length;
  const flagged = j.results.filter(r => !r.ok).length;
  statusEl.classList.remove('is-busy');
  statusEl.textContent = 'Scanned ' + total + ' HTML file(s). ' + flagged + ' contain non-base64 assets.';
  if (!total) { resultsEl.innerHTML = ''; return; }
  resultsEl.innerHTML = '<table class="data-table"><thead><tr><th style="width:90px;">Status</th><th>File</th><th style="width:90px;">Issues</th><th>Non-base64 assets</th></tr></thead><tbody></tbody></table>';
  const tbody = resultsEl.querySelector('tbody');
  for (const rr of j.results) {
    const row = document.createElement('tr');
    const assets = rr.ok ? '<span class="muted">None</span>' : '<ul class="asset-list mono">' + rr.assets.map(a => '<li>' + escapeHtml(a) + '</li>').join('') + '</ul>';
    row.innerHTML = '<td>' + (rr.ok ? '<span class="badge ok">OK</span>' : '<span class="badge err">Review</span>') + '</td>' +
      '<td class="mono wrap">' + escapeHtml(rr.file) + '</td><td>' + rr.assets.length + '</td><td class="wrap">' + assets + '</td>';
    tbody.appendChild(row);
  }
});
</script>
`
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	_, _ = w.Write([]byte(Page("/base64", "Base64 Scanner", body)))
}

func Base64PickFolder(w http.ResponseWriter, r *http.Request) {
	p := PickFolder("Select folder to scan", "")
	if p == "" {
		writeJSON(w, map[string]any{"paths": []string{}})
		return
	}
	writeJSON(w, map[string]any{"paths": collectHTMLFiles(p)})
}

func Base64PickFiles(w http.ResponseWriter, r *http.Request) {
	paths := PickFiles(
		"Select HTML files",
		[]FileFilter{{Name: "HTML", Extensions: []string{"html", "htm"}}},
		true, "",
	)
	writeJSON(w, map[string]any{"paths": paths})
}

type scanResult struct {
	File   string   `json:"file"`
	OK     bool     `json:"ok"`
	Assets []string `json:"assets"`
}

func Base64Scan(w http.ResponseWriter, r *http.Request) {
	send := newJSONStream(w)
	var req struct {
		Folder string   `json:"folder"`
		Files  []string `json:"files"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		send(map[string]any{"type": "error", "message": err.Error()})
		return
	}

	var targets []string
	var baseDir string

	if len(req.Files) > 0 {
		for _, f := range req.Files {
			if fileExists(f) {
				targets = append(targets, f)
			}
		}
		if len(targets) > 0 {
			baseDir = commonBaseDir(targets)
		}
	} else if req.Folder != "" && fileExists(req.Folder) {
		targets = collectHTMLFiles(req.Folder)
		baseDir = req.Folder
	} else {
		send(map[string]any{"type": "error", "message": "Pick a folder or files first"})
		return
	}

	results := []scanResult{}
	for i, file := range targets {
		send(map[string]any{"type": "status", "message": fmt.Sprintf("Scanning %d/%d %s", i+1, len(targets), filepath.Base(file))})
		data, err := os.ReadFile(file)
		if err != nil {
			continue
		}
		offenders := findNonBase64Assets(string(data))
		display := file
		if baseDir != "" {
			rel, err := filepath.Rel(baseDir, file)
			if err == nil && rel != "" {
				display = rel
			} else {
				display = filepath.Base(file)
			}
		}
		results = append(results, scanResult{
			File:   display,
			OK:     len(offenders) == 0,
			Assets: offenders,
		})
	}
	send(map[string]any{"type": "done", "results": results})
}

func commonBaseDir(files []string) string {
	if len(files) == 0 {
		return ""
	}
	if len(files) == 1 {
		return filepath.Dir(files[0])
	}
	split := make([][]string, len(files))
	for i, f := range files {
		split[i] = strings.Split(filepath.Dir(f), string(filepath.Separator))
	}
	first := split[0]
	i := 0
	for i < len(first) {
		match := true
		for _, parts := range split {
			if i >= len(parts) || parts[i] != first[i] {
				match = false
				break
			}
		}
		if !match {
			break
		}
		i++
	}
	return strings.Join(first[:i], string(filepath.Separator))
}

func collectHTMLFiles(root string) []string {
	var out []string
	htmlRx := regexp.MustCompile(`(?i)\.html?$`)
	_ = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return nil
		}
		if d.IsDir() {
			name := d.Name()
			if name == "node_modules" || name == ".git" {
				return filepath.SkipDir
			}
			return nil
		}
		if htmlRx.MatchString(d.Name()) {
			out = append(out, path)
		}
		return nil
	})
	return out
}

var (
	scriptBlockRx = regexp.MustCompile(`(?is)<script\b[^>]*>.*?</script>`)
	attrRx        = regexp.MustCompile(`(?i)\b(?:src|href|poster|data-src)\s*=\s*("([^"]*)"|'([^']*)')`)
	cssUrlRx      = regexp.MustCompile(`(?i)url\(\s*(?:"([^"]*)"|'([^']*)'|([^)]*))\s*\)`)
	base64DataRx  = regexp.MustCompile(`(?i)^data:[^;,]*;base64,`)
	mailTelRx     = regexp.MustCompile(`(?i)^(mailto:|tel:)`)
)

func findNonBase64Assets(htmlSrc string) []string {
	markup := scriptBlockRx.ReplaceAllString(htmlSrc, "")
	seen := map[string]bool{}
	var out []string
	add := func(u string) {
		if u == "" || seen[u] {
			return
		}
		seen[u] = true
		out = append(out, u)
	}

	for _, m := range attrRx.FindAllStringSubmatch(markup, -1) {
		url := m[2]
		if url == "" {
			url = m[3]
		}
		url = strings.TrimSpace(url)
		if url == "" {
			continue
		}
		if isAssetReference(url) && !base64DataRx.MatchString(url) {
			add(url)
		}
	}
	for _, m := range cssUrlRx.FindAllStringSubmatch(markup, -1) {
		url := m[1]
		if url == "" {
			url = m[2]
		}
		if url == "" {
			url = m[3]
		}
		url = strings.TrimSpace(url)
		if url == "" {
			continue
		}
		if !base64DataRx.MatchString(url) {
			add(url)
		}
	}
	if out == nil {
		out = []string{}
	}
	return out
}

func isAssetReference(url string) bool {
	if strings.HasPrefix(url, "#") || strings.HasPrefix(strings.ToLower(url), "javascript:") {
		return false
	}
	if mailTelRx.MatchString(url) {
		return false
	}
	return true
}
