package main

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

func Base64Page(w http.ResponseWriter, r *http.Request) {
	body := `
<h2>Base64 Asset Scanner</h2>
<div style="display:flex;gap:8px;align-items:center;margin-bottom:8px;">
  <input id="folder" type="text" placeholder="Folder to scan recursively..." style="flex:1;" />
  <button id="pickFolder">Browse Folder...</button>
  <button id="pickFiles">Pick Files...</button>
  <button id="scan">Scan</button>
</div>
<div id="selection" class="hint"></div>
<div id="status" style="margin-top:8px;"></div>
<div id="results" style="margin-top:8px;"></div>

<style>
  .row-result { display:flex; gap:8px; padding:2px 0; font-family:Consolas, monospace; font-size:12px; }
  .mark { width:14px; flex-shrink:0; font-weight:bold; }
  .mark.ok { color:#3fb950; } .mark.bad { color:#f85149; }
  .file-name { word-break:break-all; }
  .assets { opacity:0.7; margin-left:6px; word-break:break-all; }
  .summary { opacity:0.8; margin-bottom:6px; }
</style>

<script>
const folderEl = document.getElementById('folder');
const statusEl = document.getElementById('status');
const resultsEl = document.getElementById('results');
const selectionEl = document.getElementById('selection');
let pickedFiles = [];
function clearFiles(){ pickedFiles = []; selectionEl.textContent = ''; }
folderEl.addEventListener('input', clearFiles);

document.getElementById('pickFolder').addEventListener('click', async () => {
  const r = await fetch('/api/base64/pickFolder', { method: 'POST' });
  const j = await r.json();
  if (j.path) { folderEl.value = j.path; clearFiles(); }
});
document.getElementById('pickFiles').addEventListener('click', async () => {
  const r = await fetch('/api/base64/pickFiles', { method: 'POST' });
  const j = await r.json();
  if (j.paths && j.paths.length) {
    pickedFiles = j.paths;
    folderEl.value = '';
    selectionEl.textContent = pickedFiles.length + ' file(s) selected';
  }
});
document.getElementById('scan').addEventListener('click', async () => {
  statusEl.textContent = 'Scanning...';
  resultsEl.innerHTML = '';
  let payload;
  if (pickedFiles.length) payload = { files: pickedFiles };
  else {
    const folder = folderEl.value.trim();
    if (!folder) { statusEl.textContent = 'Pick a folder or files first.'; return; }
    payload = { folder };
  }
  const r = await fetch('/api/base64/scan', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
  const j = await r.json();
  if (j.error) { statusEl.textContent = 'Error: ' + j.error; return; }
  const total = j.results.length;
  const flagged = j.results.filter(r => !r.ok).length;
  statusEl.innerHTML = '<div class="summary">Scanned ' + total + ' HTML file(s). ' + flagged + ' contain non-base64 assets.</div>';
  resultsEl.innerHTML = '';
  for (const rr of j.results) {
    const row = document.createElement('div');
    row.className = 'row-result';
    const mark = document.createElement('span');
    mark.className = 'mark ' + (rr.ok ? 'ok' : 'bad');
    mark.textContent = rr.ok ? '✓' : '✗';
    const name = document.createElement('span');
    name.className = 'file-name';
    name.textContent = rr.file;
    row.appendChild(mark); row.appendChild(name);
    if (!rr.ok) {
      const a = document.createElement('span');
      a.className = 'assets';
      a.textContent = '— ' + rr.assets.join(', ');
      row.appendChild(a);
    }
    resultsEl.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", "")
	writeJSON(w, map[string]any{"path": 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) {
	var req struct {
		Folder string   `json:"folder"`
		Files  []string `json:"files"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSON(w, map[string]any{"results": []scanResult{}, "error": 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 {
		writeJSON(w, map[string]any{"results": []scanResult{}, "error": "Pick a folder or files first"})
		return
	}

	results := []scanResult{}
	for _, file := range targets {
		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,
		})
	}
	writeJSON(w, map[string]any{"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
}
