package main

import (
	"context"
	_ "embed"
	"encoding/base64"
	"encoding/json"
	"crypto/rand"
	"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
	fileBuf  []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 := `
<h2>Send To Mobile</h2>
<p class="hint" style="margin-top:0;">
  Pick an HTML file, then start sharing. Scan the QR with a phone on the same WiFi.
  The phone gets a choice of <strong>View</strong> or <strong>Download</strong>.
</p>

<div style="margin-bottom:14px;">
  <div style="display:flex;align-items:center;gap:8px;">
    <button id="pick" class="secondary">Choose...</button>
    <span id="fileName" class="hint">(no file)</span>
  </div>
  <input type="hidden" id="filePath" />
</div>

<div style="margin-bottom:14px;">
  <button id="start">Start sharing</button>
  <button id="stop" class="secondary" disabled>Stop</button>
</div>

<div id="ifaceWrap" style="display:none;margin-bottom:14px;">
  <div class="hint">Multiple network interfaces found. Pick the one your phone can reach:</div>
  <div id="ifaceList" style="display:flex;flex-direction:column;gap:4px;margin-top:6px;"></div>
</div>

<div id="shareInfo" style="display:none;">
  <div id="qr" style="margin-top:14px;background:#fff;padding:12px;display:inline-block;border-radius:4px;"></div>
  <div class="url" id="urlText" style="margin-top:10px;padding:8px;background:#252526;border-left:3px solid #007fd4;font-family:Consolas, monospace;font-size:12px;word-break:break-all;"></div>
  <div style="margin-top:6px;display:flex;gap:6px;">
    <button id="copyUrl" class="secondary">Copy URL</button>
    <button id="openUrl" class="secondary">Open in browser</button>
  </div>
  <div class="hint" style="margin-top:8px;">
    First run on Windows may show a firewall prompt — allow access for "Private networks".
  </div>
</div>

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

<script src="/assets/qrcode.min.js"></script>
<script>
const pickBtn = document.getElementById('pick');
const startBtn = document.getElementById('start');
const stopBtn = document.getElementById('stop');
const fileNameEl = document.getElementById('fileName');
const filePathEl = document.getElementById('filePath');
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');
const urlTextEl = document.getElementById('urlText');

let currentUrls = [];
let selectedIdx = 0;

pickBtn.addEventListener('click', async () => {
  const r = await fetch('/api/mobile/pick', { method:'POST' });
  const j = await r.json();
  if (j.path) {
    filePathEl.value = j.path;
    fileNameEl.textContent = j.name;
  }
});

startBtn.addEventListener('click', async () => {
  statusEl.innerHTML = '';
  if (!filePathEl.value) {
    statusEl.innerHTML = '<span class="err">Pick an HTML file first.</span>';
    return;
  }
  startBtn.disabled = true;
  statusEl.textContent = 'Starting server...';
  const r = await fetch('/api/mobile/start', {
    method:'POST', headers:{'Content-Type':'application/json'},
    body: JSON.stringify({ path: filePathEl.value }),
  });
  const j = await r.json();
  if (j.error) {
    statusEl.innerHTML = '<span class="err">' + j.error + '</span>';
    startBtn.disabled = false;
    return;
  }
  currentUrls = j.urls;
  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.textContent = 'Stopped.';
  shareInfo.style.display = 'none';
  currentUrls = [];
  startBtn.disabled = false;
  stopBtn.disabled = true;
});

document.getElementById('copyUrl').addEventListener('click', () => {
  const url = currentUrls[selectedIdx]?.url || '';
  fetch('/api/clipboard', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:url})});
});
document.getElementById('openUrl').addEventListener('click', () => {
  const url = currentUrls[selectedIdx]?.url || '';
  fetch('/api/open', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url})});
});

function renderQr(url) {
  qrEl.innerHTML = '';
  urlTextEl.textContent = url;
  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 = '';
  urls.forEach((u, i) => {
    const id = 'iface_' + i;
    const label = document.createElement('label');
    label.style.fontSize = '12px';
    label.style.cursor = 'pointer';
    label.innerHTML = '<input type="radio" name="iface" id="' + id + '"' +
      (i === selectedIdx ? ' checked' : '') + ' /> ' +
      u.ip + ' <span style="opacity:0.6;">(' + u.iface + ')</span>';
    label.querySelector('input').addEventListener('change', () => {
      selectedIdx = i;
      renderQr(urls[i].url);
    });
    ifaceList.appendChild(label);
  });
}
</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"}}},
		false, "",
	)
	if len(picked) == 0 {
		writeJSON(w, map[string]any{"path": nil})
		return
	}
	writeJSON(w, map[string]any{"path": picked[0], "name": filepath.Base(picked[0])})
}

func MobileStart(w http.ResponseWriter, r *http.Request) {
	stopActiveShare()

	var req struct {
		Path string `json:"path"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSON(w, map[string]any{"error": err.Error()})
		return
	}
	if req.Path == "" || !fileExists(req.Path) {
		writeJSON(w, map[string]any{"error": "File missing."})
		return
	}
	fileBuf, err := os.ReadFile(req.Path)
	if err != nil {
		writeJSON(w, map[string]any{"error": "Read failed: " + err.Error()})
		return
	}
	filename := filepath.Base(req.Path)

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

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

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

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

	ips := getLanIPs()
	if len(ips) == 0 {
		_ = share.server.Close()
		writeJSON(w, map[string]any{"error": "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),
		})
	}
	writeJSON(w, map[string]any{"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.filename)))
		case "/view":
			w.Header().Set("Content-Type", "text/html; charset=utf-8")
			w.Header().Set("Cache-Control", "no-store")
			_, _ = w.Write(s.fileBuf)
		case "/file":
			safeName := safeFilename(s.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.fileBuf)))
			w.Header().Set("Cache-Control", "no-store")
			_, _ = w.Write(s.fileBuf)
		default:
			http.NotFound(w, r)
		}
	})
}

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

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

func chooserPage(filename string) string {
	escName := html.EscapeString(filename)
	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; }
  .file { font-size: 13px; opacity: 0.6; margin-bottom: 28px; 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>
  <div class="file">` + escName + `</div>
  <a class="btn view" href="view">View in browser</a>
  <a class="btn dl" href="file" download="` + escName + `">Download .html</a>
</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
}
