package main

import (
	"context"
	"crypto/rand"
	_ "embed"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"html"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"
)

//go:embed assets/qrcode.min.js
var qrcodeJS []byte

type activeShare struct {
	server   *http.Server
	listener net.Listener
	port     int
	token    string
	files    []sharedFile
}

type sharedFile struct {
	buf      []byte
	filename string
}

var (
	activeMu sync.Mutex
	active   *activeShare
)

func stopActiveShare() {
	activeMu.Lock()
	defer activeMu.Unlock()
	if active != nil {
		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		defer cancel()
		_ = active.server.Shutdown(ctx)
		active = nil
	}
}

func MobileQrScript(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
	_, _ = w.Write(qrcodeJS)
}

func MobilePage(w http.ResponseWriter, r *http.Request) {
	body := `
<header class="tool-header">
  <h2 class="tool-title">Send To Mobile</h2>
  <p class="tool-description">Pick HTML files, then start sharing. Scan the QR with a phone on the same WiFi.</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-list"></div>
    </div>
    <div class="action-row">
      <button id="start">Start</button>
      <button id="stop" class="secondary" disabled>Stop</button>
    </div>
    <div id="status" class="status-panel"></div>
  </div>
</section>

<div id="ifaceWrap" class="control-group" style="display:none;margin-bottom:14px;">
  <div class="control-label">Network Interfaces</div>
  <div id="ifaceList"></div>
</div>

<div id="shareInfo" class="result-card" style="display:none;">
  <div id="qr" style="background:#fff;padding:12px;display:inline-block;border-radius:4px;"></div>
  <div class="muted" style="margin-top:8px;font-size:12px;">
    First run on Windows may show a firewall prompt - allow access for Private networks.
  </div>
</div>

<script src="/assets/qrcode.min.js"></script>
<script>
const PAGE_SIZE = 5;
const pickFolderBtn = document.getElementById('pickFolder');
const pickBtn = document.getElementById('pick');
const clearBtn = document.getElementById('clear');
const startBtn = document.getElementById('start');
const stopBtn = document.getElementById('stop');
const fileNameEl = document.getElementById('fileName');
const fileListEl = document.getElementById('fileList');
const statusEl = document.getElementById('status');
const shareInfo = document.getElementById('shareInfo');
const ifaceWrap = document.getElementById('ifaceWrap');
const ifaceList = document.getElementById('ifaceList');
const qrEl = document.getElementById('qr');

let currentUrls = [];
let selectedIdx = 0;
let selectedFiles = [];
let selectedPaths = [];
let selectedPage = 0;

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 (!selectedFiles.length) {
    fileNameEl.textContent = '(no files selected)';
    fileListEl.innerHTML = '';
    return;
  }
  fileNameEl.textContent = selectedFiles.length + ' file' + (selectedFiles.length === 1 ? '' : 's');
  const maxPage = Math.max(0, Math.ceil(selectedFiles.length / PAGE_SIZE) - 1);
  selectedPage = Math.min(selectedPage, maxPage);
  const start = selectedPage * PAGE_SIZE;
  const visible = selectedFiles.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>' +
    visible.map((file, offset) => {
      const i = start + offset;
      return '<tr><td class="mono wrap">' + escapeHtml(file.name) + '</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', () => {
      const index = Number(btn.dataset.index);
      selectedFiles.splice(index, 1);
      selectedPaths.splice(index, 1);
      renderSelection();
    });
  });
  if (selectedFiles.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(); });
  }
}
function addFiles(files) {
  const existing = new Set(selectedFiles.map(f => f.path));
  selectedFiles = selectedFiles.concat((files || []).filter(f => !existing.has(f.path)));
  selectedPaths = selectedFiles.map(f => f.path);
  selectedPage = Math.max(0, Math.ceil(selectedFiles.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) => {
  addFiles(j.files || []);
  if (j.skipped) statusEl.textContent = 'Skipped ' + j.skipped + ' non-HTML or unavailable item(s).';
});

pickFolderBtn.addEventListener('click', async () => {
  const r = await fetch('/api/mobile/pickFolder', { method:'POST' });
  const j = await r.json();
  addFiles(j.files || []);
});
pickBtn.addEventListener('click', async () => {
  const r = await fetch('/api/mobile/pick', { method:'POST' });
  const j = await r.json();
  addFiles(j.files || []);
});
clearBtn.addEventListener('click', () => {
  selectedFiles = [];
  selectedPaths = [];
  selectedPage = 0;
  statusEl.textContent = '';
  statusEl.classList.remove('is-busy');
  renderSelection();
});

startBtn.addEventListener('click', async () => {
  statusEl.innerHTML = '';
  if (!selectedPaths.length) {
    statusEl.innerHTML = '<span class="err">Pick at least one HTML file first.</span>';
    return;
  }
  startBtn.disabled = true;
  statusEl.textContent = 'Starting server...';
  statusEl.classList.add('is-busy');
  let j = null;
  try {
    const r = await fetch('/api/mobile/start', {
      method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify({ paths: selectedPaths }),
    });
    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.innerHTML = '<span class="err">' + (e.message || e) + '</span>';
    startBtn.disabled = false;
    return;
  }
  if (!j) { statusEl.classList.remove('is-busy'); statusEl.innerHTML = '<span class="err">No response.</span>'; startBtn.disabled = false; return; }
  if (j.error) {
    statusEl.classList.remove('is-busy');
    statusEl.innerHTML = '<span class="err">' + j.error + '</span>';
    startBtn.disabled = false;
    return;
  }
  currentUrls = j.urls;
  statusEl.classList.remove('is-busy');
  selectedIdx = 0;
  statusEl.innerHTML = '<span class="ok">Sharing: ' + j.filename + '</span>';
  shareInfo.style.display = '';
  renderIfaces(j.urls);
  renderQr(j.urls[0].url);
  stopBtn.disabled = false;
});

stopBtn.addEventListener('click', async () => {
  await fetch('/api/mobile/stop', { method:'POST' });
  statusEl.classList.remove('is-busy');
  statusEl.textContent = 'Stopped.';
  shareInfo.style.display = 'none';
  currentUrls = [];
  startBtn.disabled = false;
  stopBtn.disabled = true;
});

function renderQr(url) {
  qrEl.innerHTML = '';
  try {
    const qr = qrcode(0, 'M');
    qr.addData(url);
    qr.make();
    qrEl.innerHTML = qr.createSvgTag({ cellSize: 5, margin: 2, scalable: true });
    const svg = qrEl.querySelector('svg');
    if (svg) { svg.setAttribute('width','240'); svg.setAttribute('height','240'); }
  } catch (e) {
    qrEl.textContent = 'QR render failed: ' + e.message;
  }
}

function renderIfaces(urls) {
  ifaceList.innerHTML = '';
  if (urls.length <= 1) { ifaceWrap.style.display = 'none'; return; }
  ifaceWrap.style.display = '';
  ifaceList.innerHTML = '<div class="results-panel" style="margin-top:0;"><table class="data-table"><thead><tr><th style="width:70px;">Use</th><th style="width:140px;">IP</th><th>Interface</th></tr></thead><tbody></tbody></table></div>';
  const tbody = ifaceList.querySelector('tbody');
  urls.forEach((u, i) => {
    const id = 'iface_' + i;
    const row = document.createElement('tr');
    row.innerHTML = '<td style="text-align:center;"><input type="radio" name="iface" id="' + id + '"' + (i === selectedIdx ? ' checked' : '') + ' /></td>' +
      '<td class="mono wrap">' + u.ip + '</td><td class="wrap">' + u.iface + '</td>';
    row.querySelector('input').addEventListener('change', () => {
      selectedIdx = i;
      renderQr(urls[i].url);
    });
    tbody.appendChild(row);
  });
}
</script>
`
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	_, _ = w.Write([]byte(Page("/mobile", "Send To Mobile", body)))
}

func MobilePick(w http.ResponseWriter, r *http.Request) {
	picked := PickFiles(
		"Select HTML",
		[]FileFilter{{Name: "HTML", Extensions: []string{"html", "htm"}}},
		true, "",
	)
	if len(picked) == 0 {
		writeJSON(w, map[string]any{"files": []any{}})
		return
	}
	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 MobilePickFolder(w http.ResponseWriter, r *http.Request) {
	dir := PickFolder("Select folder", "")
	if dir == "" {
		writeJSON(w, map[string]any{"files": []any{}})
		return
	}
	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 MobileStart(w http.ResponseWriter, r *http.Request) {
	stopActiveShare()
	send := newJSONStream(w)

	var req struct {
		Path  string   `json:"path"`
		Paths []string `json:"paths"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		send(map[string]any{"type": "error", "message": err.Error()})
		return
	}
	paths := req.Paths
	if len(paths) == 0 && req.Path != "" {
		paths = []string{req.Path}
	}
	if len(paths) == 0 {
		send(map[string]any{"type": "error", "message": "File missing."})
		return
	}
	files := make([]sharedFile, 0, len(paths))
	for i, p := range paths {
		if p == "" || !fileExists(p) {
			send(map[string]any{"type": "error", "message": "File missing."})
			return
		}
		send(map[string]any{"type": "status", "message": fmt.Sprintf("Loading %d/%d %s", i+1, len(paths), filepath.Base(p))})
		fileBuf, err := os.ReadFile(p)
		if err != nil {
			send(map[string]any{"type": "error", "message": "Read failed: " + err.Error()})
			return
		}
		files = append(files, sharedFile{buf: fileBuf, filename: filepath.Base(p)})
	}

	// 6 random bytes -> base64url, no padding
	tokenBytes := make([]byte, 6)
	if _, err := rand.Read(tokenBytes); err != nil {
		send(map[string]any{"type": "error", "message": "Token gen failed: " + err.Error()})
		return
	}
	token := strings.TrimRight(base64.URLEncoding.EncodeToString(tokenBytes), "=")

	cfgPort := LoadConfig().SendToMobile.Port
	send(map[string]any{"type": "status", "message": "Starting server..."})
	listener, err := net.Listen("tcp", "0.0.0.0:"+strconv.Itoa(cfgPort))
	if err != nil {
		send(map[string]any{"type": "error", "message": "Failed to bind: " + err.Error()})
		return
	}
	port := listener.Addr().(*net.TCPAddr).Port

	share := &activeShare{
		listener: listener,
		port:     port,
		token:    token,
		files:    files,
	}
	share.server = &http.Server{Handler: shareHandler(share)}

	go func() { _ = share.server.Serve(listener) }()

	ips := getLanIPs()
	if len(ips) == 0 {
		_ = share.server.Close()
		send(map[string]any{"type": "error", "message": "No non-loopback IPv4 interface found. Are you on a network?"})
		return
	}

	activeMu.Lock()
	active = share
	activeMu.Unlock()

	type urlInfo struct {
		IP    string `json:"ip"`
		Iface string `json:"iface"`
		URL   string `json:"url"`
	}
	urls := make([]urlInfo, 0, len(ips))
	for _, ip := range ips {
		urls = append(urls, urlInfo{
			IP:    ip.address,
			Iface: ip.iface,
			URL:   fmt.Sprintf("http://%s:%d/s/%s/", ip.address, port, token),
		})
	}
	filename := fmt.Sprintf("%d files", len(files))
	if len(files) == 1 {
		filename = files[0].filename
	}
	send(map[string]any{"type": "done", "urls": urls, "filename": filename})
}

func MobileStop(w http.ResponseWriter, r *http.Request) {
	stopActiveShare()
	writeJSON(w, map[string]any{"ok": true})
}

func shareHandler(s *activeShare) http.Handler {
	prefix := "/s/" + s.token
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet || !strings.HasPrefix(r.URL.Path, prefix) {
			http.NotFound(w, r)
			return
		}
		rest := strings.TrimPrefix(r.URL.Path, prefix)
		switch rest {
		case "", "/":
			w.Header().Set("Content-Type", "text/html; charset=utf-8")
			w.Header().Set("Cache-Control", "no-store")
			_, _ = w.Write([]byte(chooserPage(s.files)))
		case "/view":
			fallthrough
		case "/view/0":
			if len(s.files) == 0 {
				http.NotFound(w, r)
				return
			}
			w.Header().Set("Content-Type", "text/html; charset=utf-8")
			w.Header().Set("Cache-Control", "no-store")
			_, _ = w.Write(s.files[0].buf)
		case "/file":
			fallthrough
		case "/file/0":
			if len(s.files) == 0 {
				http.NotFound(w, r)
				return
			}
			safeName := safeFilename(s.files[0].filename)
			w.Header().Set("Content-Type", "application/octet-stream")
			w.Header().Set("Content-Disposition", `attachment; filename="`+safeName+`"`)
			w.Header().Set("Content-Length", strconv.Itoa(len(s.files[0].buf)))
			w.Header().Set("Cache-Control", "no-store")
			_, _ = w.Write(s.files[0].buf)
		default:
			if strings.HasPrefix(rest, "/view/") || strings.HasPrefix(rest, "/file/") {
				parts := strings.Split(strings.Trim(rest, "/"), "/")
				if len(parts) == 2 {
					idx, err := strconv.Atoi(parts[1])
					if err == nil && idx >= 0 && idx < len(s.files) {
						file := s.files[idx]
						if parts[0] == "view" {
							w.Header().Set("Content-Type", "text/html; charset=utf-8")
							w.Header().Set("Cache-Control", "no-store")
							_, _ = w.Write(file.buf)
							return
						}
						safeName := safeFilename(file.filename)
						w.Header().Set("Content-Type", "application/octet-stream")
						w.Header().Set("Content-Disposition", `attachment; filename="`+safeName+`"`)
						w.Header().Set("Content-Length", strconv.Itoa(len(file.buf)))
						w.Header().Set("Cache-Control", "no-store")
						_, _ = w.Write(file.buf)
						return
					}
				}
			}
			http.NotFound(w, r)
		}
	})
}

var unsafeFilenameRx = regexp.MustCompile(`[^A-Za-z0-9._-]`)

func safeFilename(s string) string { return unsafeFilenameRx.ReplaceAllString(s, "_") }

func chooserPage(files []sharedFile) string {
	var items strings.Builder
	for i, file := range files {
		escName := html.EscapeString(file.filename)
		items.WriteString(`<div class="item"><div class="file">` + escName + `</div>
  <a class="btn view" href="view/` + strconv.Itoa(i) + `">View in browser</a>
  <a class="btn dl" href="file/` + strconv.Itoa(i) + `" download="` + escName + `">Download .html</a></div>`)
	}
	return `<!DOCTYPE html>
<html><head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Send To Mobile</title>
<style>
  body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
         margin: 0; padding: 24px; background: #111; color: #eee; }
  h1 { font-size: 18px; font-weight: 500; margin: 0 0 4px; opacity: 0.85; }
  .item { margin-bottom: 26px; }
  .file { font-size: 13px; opacity: 0.6; margin-bottom: 10px; word-break: break-all; }
  a.btn { display: block; padding: 18px; margin-bottom: 14px; border-radius: 10px;
          font-size: 17px; font-weight: 500; text-align: center; text-decoration: none; }
  a.view { background: #2d7dff; color: white; }
  a.dl { background: #2a2a2a; color: #eee; border: 1px solid #3a3a3a; }
</style></head>
<body>
  <h1>HPL Toolbox</h1>
  ` + items.String() + `
</body></html>`
}

type lanIP struct {
	address string
	iface   string
}

func getLanIPs() []lanIP {
	var out []lanIP
	ifaces, err := net.Interfaces()
	if err != nil {
		return out
	}
	for _, iface := range ifaces {
		if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
			continue
		}
		addrs, err := iface.Addrs()
		if err != nil {
			continue
		}
		for _, a := range addrs {
			ipNet, ok := a.(*net.IPNet)
			if !ok {
				continue
			}
			ip4 := ipNet.IP.To4()
			if ip4 == nil || ip4.IsLoopback() {
				continue
			}
			out = append(out, lanIP{address: ip4.String(), iface: iface.Name})
		}
	}
	return out
}
