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 := `
<h2>PLEC Upload</h2>
<p class="hint" style="margin-top:0;">Pick HTML files, give each an iteration name, then upload. The preview link is returned by the server.</p>

<table id="rows" style="width:100%;border-collapse:collapse;margin-bottom:12px;">
  <thead><tr>
    <th style="width:45%;text-align:left;padding:6px 8px;font-size:12px;text-transform:uppercase;opacity:0.7;">HTML File</th>
    <th style="width:45%;text-align:left;padding:6px 8px;font-size:12px;text-transform:uppercase;opacity:0.7;">Iteration Name</th>
    <th></th>
  </tr></thead>
  <tbody></tbody>
</table>

<div style="display:flex;gap:8px;">
  <button id="addRow" class="secondary">+ Add Row</button>
  <button id="upload">Upload</button>
</div>

<div id="status" style="margin-top:12px;min-height:20px;"></div>
<div id="results"></div>

<style>
  #rows td { padding:6px 8px; vertical-align:middle; border-bottom:1px solid #333; }
  .file-cell { display:flex; align-items:center; gap:8px; }
  .file-name { opacity:0.85; font-size:12px; word-break:break-all; }
  .row-error { color:#f48771; font-size:12px; margin-top:4px; }
  .result { margin-top:12px; padding:10px; background:#252526; border-left:3px solid #007fd4; word-break:break-all; }
  .result a { color:#9cdcfe; }
  .result-actions { margin-top:8px; display:flex; gap:8px; }
  .remove-btn { background:transparent; color:#ddd; padding:4px 8px; }
</style>

<script>
let nextId = 1;
const tbody = document.querySelector('#rows tbody');
const statusEl = document.getElementById('status');
const resultsEl = document.getElementById('results');

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 addRow() {
  const id = 'r' + (nextId++);
  const tr = document.createElement('tr');
  tr.dataset.id = id;
  tr.innerHTML = ` + "`" + `
    <td>
      <div class="file-cell">
        <button class="pick secondary">Choose...</button>
        <span class="file-name" data-name>(no file)</span>
      </div>
      <input type="hidden" data-path />
      <div class="row-error" data-error></div>
    </td>
    <td><input type="text" data-iter placeholder="iteration name" style="width:100%;" /></td>
    <td><button class="remove-btn" title="Remove">×</button></td>
  ` + "`" + `;
  tr.querySelector('.pick').addEventListener('click', async () => {
    const res = await fetch('/api/plec/pick', { method: 'POST' });
    const j = await res.json();
    if (j.files && j.files.length) {
      const affected = [];
      j.files.forEach(function(f, i) {
        const target = i === 0 ? tr : (addRow(), tbody.lastElementChild);
        target.querySelector('[data-path]').value = f.path;
        target.querySelector('[data-name]').textContent = f.name;
        const iter = target.querySelector('[data-iter]');
        if (!iter.value) iter.value = parseIterationName(f.name);
        affected.push(target);
      });
      if (affected.length > 1) {
        const iters = affected.map(r => r.querySelector('[data-iter]').value);
        if (iters[0] !== '' && iters.every(n => n === iters[0])) {
          affected.forEach((r, i) => {
            r.querySelector('[data-iter]').value = String(i + 1).padStart(2, '0') + '_' + iters[0];
          });
        }
      }
    }
  });
  tr.querySelector('.remove-btn').addEventListener('click', () => {
    tr.remove();
    if (!tbody.children.length) addRow();
  });
  tbody.appendChild(tr);
}

document.getElementById('addRow').addEventListener('click', addRow);

document.getElementById('upload').addEventListener('click', async () => {
  document.querySelectorAll('[data-error]').forEach(el => el.textContent = '');
  statusEl.textContent = '';
  resultsEl.innerHTML = '';
  const rows = [...tbody.querySelectorAll('tr')].map(tr => ({
    id: tr.dataset.id,
    path: tr.querySelector('[data-path]').value,
    iteration: tr.querySelector('[data-iter]').value,
  })).filter(r => r.path || r.iteration);
  if (rows.length === 0) {
    statusEl.innerHTML = '<span class="err">Add at least one row with a file and iteration name.</span>';
    return;
  }
  statusEl.textContent = 'Uploading...';
  const res = await fetch('/api/plec/upload', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ rows }),
  });
  const j = await res.json();
  if (j.rowErrors) {
    for (const re of j.rowErrors) {
      const tr = tbody.querySelector('tr[data-id="' + re.id + '"]');
      if (tr) tr.querySelector('[data-error]').textContent = re.message;
    }
    statusEl.innerHTML = '<span class="err">Fix row errors and retry.</span>';
    return;
  }
  if (j.error) {
    statusEl.innerHTML = '<span class="err">' + j.error + '</span>';
    return;
  }
  statusEl.innerHTML = '<span class="ok">Upload complete.</span>';
  const wrap = document.createElement('div');
  wrap.className = 'result';
  if (j.preview) {
    wrap.innerHTML = '<div><strong>Preview:</strong> <a href="' + j.preview + '" target="_blank">' + j.preview + '</a></div>';
    const actions = document.createElement('div');
    actions.className = 'result-actions';
    const copy = document.createElement('button');
    copy.textContent = 'Copy';
    copy.onclick = () => fetch('/api/clipboard', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ text: j.preview }) });
    const open = document.createElement('button');
    open.textContent = 'Open';
    open.className = 'secondary';
    open.onclick = () => fetch('/api/open', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ url: j.preview }) });
    const showRaw = document.createElement('button');
    showRaw.textContent = 'Show server response';
    showRaw.className = 'secondary';
    showRaw.onclick = () => {
      const pre = document.createElement('pre');
      pre.style.marginTop = '8px';
      pre.style.whiteSpace = 'pre-wrap';
      pre.textContent = j.rawText;
      wrap.appendChild(pre);
      showRaw.disabled = true;
    };
    actions.appendChild(copy); actions.appendChild(open); actions.appendChild(showRaw);
    wrap.appendChild(actions);
  } else {
    wrap.innerHTML = '<div>No preview field in response. Raw:</div><pre>' + (j.rawText || '').replace(/</g,'&lt;') + '</pre>';
  }
  resultsEl.innerHTML = '';
  resultsEl.appendChild(wrap);
});

addRow();
</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})
}

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

func PlecUpload(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Rows []plecRow `json:"rows"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSON(w, map[string]any{"error": 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 {
		writeJSON(w, map[string]any{"rowErrors": rowErrors})
		return
	}
	if len(valid) == 0 {
		writeJSON(w, map[string]any{"error": "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
		data, err := os.ReadFile(row.Path)
		if err != nil {
			writeJSON(w, map[string]any{"error": "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 {
			writeJSON(w, map[string]any{"error": err.Error()})
			return
		}
		if _, err := fw.Write(data); err != nil {
			writeJSON(w, map[string]any{"error": 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 {
			writeJSON(w, map[string]any{"error": err.Error()})
			return
		}
	}
	if err := mw.Close(); err != nil {
		writeJSON(w, map[string]any{"error": err.Error()})
		return
	}

	httpReq, err := http.NewRequest("POST", cfg.UploadUrl, &buf)
	if err != nil {
		writeJSON(w, map[string]any{"error": 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 {
		writeJSON(w, map[string]any{"error": "Network error: " + err.Error()})
		return
	}
	defer resp.Body.Close()
	respBody, _ := io.ReadAll(resp.Body)
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		writeJSON(w, map[string]any{"error": 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 {
		writeJSON(w, map[string]any{"error": "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
	}
	writeJSON(w, map[string]any{"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()
}
