package main

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

func PlecPage(w http.ResponseWriter, r *http.Request) {
	body := `
<header class="tool-header">
  <h2 class="tool-title">PLEC Upload</h2>
  <p class="tool-description">Pick HTML files, give each an iteration name, then upload them to PLEC.</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="emptySelection" class="file-name" style="margin-top:8px;">(no files selected)</div>
      <div id="rowsPanel" class="results-panel is-hidden" style="margin-top:8px;">
        <table id="rows" class="data-table">
          <thead><tr><th>Filename</th><th style="width:240px;">Iteration Name</th><th style="width:44px;"></th></tr></thead>
          <tbody></tbody>
        </table>
      </div>
    </div>
    <div class="action-row">
      <button id="upload">Upload</button>
      <button id="openResult" class="secondary" disabled>Open</button>
      <button id="copyResult" class="secondary" disabled>Copy Link</button>
    </div>
    <div id="status" class="status-panel"></div>
  </div>
</section>

<style>
  .row-error { color:#f48771; font-size:12px; margin-top:4px; }
  #rows input[type=text] { width: 100%; }
</style>

<script>
const PAGE_SIZE = 5;
let nextId = 1;
let rows = [];
let page = 0;
let previewUrl = '';
const tbody = document.querySelector('#rows tbody');
const statusEl = document.getElementById('status');
const rowsPanel = document.getElementById('rowsPanel');
const emptySelection = document.getElementById('emptySelection');
const openResultBtn = document.getElementById('openResult');
const copyResultBtn = document.getElementById('copyResult');

function parseIterationName(filename) {
  const base = filename.replace(/\.html?$/i, '');
  const tokens = base.split('_');
  for (const t of tokens) {
    if (/^(full|\d+clk|\d+s(?:ec)?)$/i.test(t)) return t.toLowerCase();
  }
  return base;
}

function escapeHtml(s){
  return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
function addFiles(files) {
  const existing = new Set(rows.map(r => r.path));
  const before = rows.length;
  (files || []).forEach(f => {
    if (existing.has(f.path)) return;
    rows.push({ id:'r' + (nextId++), path:f.path, name:f.name, iteration:parseIterationName(f.name), error:'' });
    existing.add(f.path);
  });
  const added = rows.length - before;
  if (added > 1) {
    const batch = rows.slice(rows.length - added);
    const first = batch[0].iteration;
    if (first && batch.every(r => r.iteration === first)) {
      batch.forEach((r, i) => r.iteration = String(i + 1).padStart(2, '0') + '_' + first);
    }
  }
  page = Math.max(0, Math.ceil(rows.length / PAGE_SIZE) - 1);
  renderRows();
}
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 renderRows() {
  const oldPager = document.getElementById('selectedPager');
  if (oldPager) oldPager.remove();
  rowsPanel.classList.toggle('is-hidden', rows.length === 0);
  emptySelection.classList.toggle('is-hidden', rows.length !== 0);
  if (!rows.length) { tbody.innerHTML = ''; return; }
  const maxPage = Math.max(0, Math.ceil(rows.length / PAGE_SIZE) - 1);
  page = Math.min(page, maxPage);
  const start = page * PAGE_SIZE;
  const pageRows = rows.slice(start, start + PAGE_SIZE);
  tbody.innerHTML = pageRows.map((row, offset) => {
    const i = start + offset;
    return '<tr data-id="' + row.id + '">' +
      '<td><span class="mono wrap">' + escapeHtml(row.name) + '</span><div class="row-error">' + escapeHtml(row.error || '') + '</div></td>' +
      '<td><input type="text" data-iter data-index="' + i + '" value="' + escapeHtml(row.iteration) + '" /></td>' +
      '<td><button class="remove-btn danger" data-remove="' + i + '" title="Remove">×</button></td>' +
      '</tr>';
  }).join('');
  tbody.querySelectorAll('[data-iter]').forEach(input => {
    input.addEventListener('input', () => rows[Number(input.dataset.index)].iteration = input.value);
  });
  tbody.querySelectorAll('[data-remove]').forEach(btn => {
    btn.addEventListener('click', () => { rows.splice(Number(btn.dataset.remove), 1); renderRows(); });
  });
  if (rows.length > PAGE_SIZE) {
    const pager = document.createElement('div');
    pager.id = 'selectedPager';
    pager.className = 'pager';
    pager.innerHTML = '<button class="secondary" id="prevPage"' + (page === 0 ? ' disabled' : '') + '>Previous</button>' +
      '<span class="file-name">Page ' + (page + 1) + ' of ' + (maxPage + 1) + '</span>' +
      '<button class="secondary" id="nextPage"' + (page === maxPage ? ' disabled' : '') + '>Next</button>';
    rowsPanel.after(pager);
    pager.querySelector('#prevPage').addEventListener('click', () => { page--; renderRows(); });
    pager.querySelector('#nextPage').addEventListener('click', () => { page++; renderRows(); });
  }
}
function setPreview(url) {
  previewUrl = url || '';
  openResultBtn.disabled = !previewUrl;
  copyResultBtn.disabled = !previewUrl;
}
setupDropZone('dropZone', statusEl, (j) => {
  addFiles(j.files || []);
  if (j.skipped) statusEl.textContent = 'Skipped ' + j.skipped + ' non-HTML or unavailable item(s).';
});

document.getElementById('pickFolder').addEventListener('click', async () => {
  const res = await fetch('/api/plec/pickFolder', { method: 'POST' });
  const j = await res.json();
  addFiles(j.files || []);
});
document.getElementById('pickFiles').addEventListener('click', async () => {
  const res = await fetch('/api/plec/pick', { method: 'POST' });
  const j = await res.json();
  addFiles(j.files || []);
});
document.getElementById('clear').addEventListener('click', () => {
  rows = [];
  page = 0;
  setPreview('');
  statusEl.textContent = '';
  statusEl.classList.remove('is-busy');
  renderRows();
});
openResultBtn.addEventListener('click', () => {
  if (previewUrl) fetch('/api/open', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ url: previewUrl }) });
});
copyResultBtn.addEventListener('click', () => {
  if (previewUrl) fetch('/api/clipboard', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ text: previewUrl }) });
});

document.getElementById('upload').addEventListener('click', async () => {
  rows.forEach(row => row.error = '');
  renderRows();
  statusEl.textContent = '';
  statusEl.classList.remove('is-busy');
  setPreview('');
  if (rows.length === 0) {
    statusEl.innerHTML = '<span class="err">Select at least one file.</span>';
    return;
  }
  statusEl.textContent = 'Uploading...';
  statusEl.classList.add('is-busy');
  let j = null;
  try {
    const res = await fetch('/api/plec/upload', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ rows }),
    });
    await readJSONStream(res, (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 };
      } else if (msg.type === 'rowErrors') {
        j = { rowErrors: msg.rowErrors };
      }
    });
  } catch (e) {
    statusEl.classList.remove('is-busy');
    statusEl.innerHTML = '<span class="err">' + (e.message || e) + '</span>';
    return;
  }
  if (!j) { statusEl.classList.remove('is-busy'); statusEl.innerHTML = '<span class="err">No response.</span>'; return; }
  if (j.rowErrors) {
    statusEl.classList.remove('is-busy');
    for (const re of j.rowErrors) {
      const row = rows.find(r => r.id === re.id);
      if (row) row.error = re.message;
    }
    renderRows();
    statusEl.innerHTML = '<span class="err">Fix row errors and retry.</span>';
    return;
  }
  if (j.error) {
    statusEl.classList.remove('is-busy');
    statusEl.innerHTML = '<span class="err">' + j.error + '</span>';
    return;
  }
  statusEl.classList.remove('is-busy');
  statusEl.innerHTML = '<span class="ok">Upload complete.</span>';
  setPreview(j.preview);
});

renderRows();
</script>
`
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	_, _ = w.Write([]byte(Page("/plec", "PLEC Upload", body)))
}

func PlecPick(w http.ResponseWriter, r *http.Request) {
	cfg := LoadConfig()
	picked := PickFiles(
		"Select HTML",
		[]FileFilter{{Name: "HTML", Extensions: []string{"html", "htm"}}},
		true, cfg.LastPickDir,
	)
	if len(picked) == 0 {
		writeJSON(w, map[string]any{"files": []any{}})
		return
	}
	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 PlecPickFolder(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})
}

type plecRow struct {
	ID        string `json:"id"`
	Path      string `json:"path"`
	Iteration string `json:"iteration"`
}

func PlecUpload(w http.ResponseWriter, r *http.Request) {
	send := newJSONStream(w)
	var req struct {
		Rows []plecRow `json:"rows"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		send(map[string]any{"type": "error", "message": err.Error()})
		return
	}
	cfg := LoadConfig().Plec

	type rowErr struct {
		ID      string `json:"id"`
		Message string `json:"message"`
	}
	var rowErrors []rowErr
	var valid []plecRow
	htmlRx := regexp.MustCompile(`(?i)\.html?$`)

	for _, row := range req.Rows {
		if row.Path == "" || !fileExists(row.Path) {
			rowErrors = append(rowErrors, rowErr{row.ID, "File missing"})
		} else if strings.TrimSpace(row.Iteration) == "" {
			rowErrors = append(rowErrors, rowErr{row.ID, "Iteration name required"})
		} else if !htmlRx.MatchString(row.Path) {
			rowErrors = append(rowErrors, rowErr{row.ID, "Must be .html"})
		} else {
			valid = append(valid, row)
		}
	}
	if len(rowErrors) > 0 {
		send(map[string]any{"type": "rowErrors", "rowErrors": rowErrors})
		return
	}
	if len(valid) == 0 {
		send(map[string]any{"type": "error", "message": "Nothing to upload."})
		return
	}

	stamp := time.Now().Format("20060102150405")

	var buf bytes.Buffer
	mw := multipart.NewWriter(&buf)
	for i, row := range valid {
		idx := i + 1
		send(map[string]any{"type": "status", "message": fmt.Sprintf("Preparing %d/%d %s", idx, len(valid), filepath.Base(row.Path))})
		data, err := os.ReadFile(row.Path)
		if err != nil {
			send(map[string]any{"type": "error", "message": "Read failed: " + err.Error()})
			return
		}
		stampedName := appendTimestamp(filepath.Base(row.Path), stamp)
		fw, err := createFormFileWithType(mw, fmt.Sprintf("fileUpload_%d", idx), stampedName, "text/html")
		if err != nil {
			send(map[string]any{"type": "error", "message": err.Error()})
			return
		}
		if _, err := fw.Write(data); err != nil {
			send(map[string]any{"type": "error", "message": err.Error()})
			return
		}
		iter := urlpkg.QueryEscape(strings.ReplaceAll(row.Iteration, " ", ""))
		// Original uses encodeURI which keeps more chars than QueryEscape; emulate by un-escaping safe chars.
		iter = encodeURICompatible(strings.ReplaceAll(row.Iteration, " ", ""))
		if err := mw.WriteField(fmt.Sprintf("iterationNameUpload_%d", idx), iter); err != nil {
			send(map[string]any{"type": "error", "message": err.Error()})
			return
		}
	}
	if err := mw.Close(); err != nil {
		send(map[string]any{"type": "error", "message": err.Error()})
		return
	}

	send(map[string]any{"type": "status", "message": fmt.Sprintf("Uploading %d file%s...", len(valid), pluralS(len(valid)))})
	httpReq, err := http.NewRequest("POST", cfg.UploadUrl, &buf)
	if err != nil {
		send(map[string]any{"type": "error", "message": err.Error()})
		return
	}
	httpReq.Header.Set("Content-Type", mw.FormDataContentType())
	httpReq.Header.Set("Origin", cfg.OriginUrl)
	httpReq.Header.Set("Referer", cfg.OriginUrl+"/")
	httpReq.Header.Set("User-Agent", "Mozilla/5.0 (HPL-Toolbox-Standalone)")
	httpReq.Header.Set("X-Requested-With", "XMLHttpRequest")
	httpReq.Header.Set("Accept", "*/*")

	resp, err := http.DefaultClient.Do(httpReq)
	if err != nil {
		send(map[string]any{"type": "error", "message": "Network error: " + err.Error()})
		return
	}
	defer resp.Body.Close()
	respBody, _ := io.ReadAll(resp.Body)
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		send(map[string]any{"type": "error", "message": fmt.Sprintf("HTTP %d\n%s", resp.StatusCode, truncate(string(respBody), 800))})
		return
	}
	var parsed map[string]any
	if err := json.Unmarshal(respBody, &parsed); err != nil {
		send(map[string]any{"type": "error", "message": "Server returned non-JSON:\n" + truncate(string(respBody), 800)})
		return
	}
	var preview any
	if v, ok := parsed["preview"]; ok && v != nil {
		preview = v
	} else if v, ok := parsed["previewURL"]; ok {
		preview = v
	}
	send(map[string]any{"type": "done", "preview": preview, "raw": parsed, "rawText": string(respBody)})
}

func ClipboardEndpoint(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Text string `json:"text"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSON(w, map[string]any{"ok": false, "error": err.Error()})
		return
	}
	if err := CopyToClipboard(req.Text); err != nil {
		writeJSON(w, map[string]any{"ok": false, "error": err.Error()})
		return
	}
	writeJSON(w, map[string]any{"ok": true})
}

func OpenEndpoint(w http.ResponseWriter, r *http.Request) {
	var req struct {
		URL string `json:"url"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSON(w, map[string]any{"ok": false, "error": err.Error()})
		return
	}
	OpenInBrowser(req.URL)
	writeJSON(w, map[string]any{"ok": true})
}

func fileExists(p string) bool {
	_, err := os.Stat(p)
	return err == nil
}

func appendTimestamp(filename, stamp string) string {
	ext := filepath.Ext(filename)
	base := filename
	if ext != "" {
		base = filename[:len(filename)-len(ext)]
	}
	return base + "_" + stamp + ext
}

func truncate(s string, n int) string {
	if len(s) <= n {
		return s
	}
	return s[:n]
}

// createFormFileWithType is like multipart.Writer.CreateFormFile but lets us
// override the Content-Type (CreateFormFile hardcodes "application/octet-stream").
func createFormFileWithType(mw *multipart.Writer, field, filename, contentType string) (io.Writer, error) {
	h := make(map[string][]string)
	h["Content-Disposition"] = []string{
		fmt.Sprintf(`form-data; name=%q; filename=%q`, field, filename),
	}
	h["Content-Type"] = []string{contentType}
	return mw.CreatePart(h)
}

// encodeURICompatible mimics JS encodeURI() — keeps A-Za-z0-9 and these
// reserved/mark chars: ;,/?:@&=+$-_.!~*'()#
func encodeURICompatible(s string) string {
	keep := func(c byte) bool {
		if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') {
			return true
		}
		switch c {
		case ';', ',', '/', '?', ':', '@', '&', '=', '+', '$',
			'-', '_', '.', '!', '~', '*', '\'', '(', ')', '#':
			return true
		}
		return false
	}
	var b strings.Builder
	for i := 0; i < len(s); i++ {
		c := s[i]
		if keep(c) {
			b.WriteByte(c)
		} else {
			b.WriteString(fmt.Sprintf("%%%02X", c))
		}
	}
	return b.String()
}
