package main

import (
	"crypto/ed25519"
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"html/template"
	"log"
	"net/http"
	"os"
	"strings"
	"time"
)

const appVersion = "1.0.0"

type ActivationRequest struct {
	App              string   `json:"app"`
	Version          string   `json:"version"`
	GeneratedAt      string   `json:"generated_at"`
	Hostname         string   `json:"hostname"`
	Platform         string   `json:"platform"`
	Fingerprint      string   `json:"fingerprint"`
	FingerprintShort string   `json:"fingerprint_short"`
	Components       []string `json:"components"`
}

type SignedLicense struct {
	App         string   `json:"app"`
	LicenseID   string   `json:"license_id"`
	Customer    string   `json:"customer"`
	Fingerprint string   `json:"fingerprint"`
	IssuedAt    string   `json:"issued_at"`
	ExpiresAt   string   `json:"expires_at,omitempty"`
	Features    []string `json:"features,omitempty"`
	Signature   string   `json:"signature"`
}

type licensePayload struct {
	App         string   `json:"app"`
	LicenseID   string   `json:"license_id"`
	Customer    string   `json:"customer"`
	Fingerprint string   `json:"fingerprint"`
	IssuedAt    string   `json:"issued_at"`
	ExpiresAt   string   `json:"expires_at,omitempty"`
	Features    []string `json:"features,omitempty"`
}

type keyPairResponse struct {
	PublicKeyBase64  string `json:"public_key_base64"`
	PrivateKeyBase64 string `json:"private_key_base64"`
}

type generateLicenseRequest struct {
	PrivateKeyBase64 string `json:"private_key_base64"`
	App              string `json:"app"`
	LicenseID        string `json:"license_id"`
	Customer         string `json:"customer"`
	Fingerprint      string `json:"fingerprint"`
	IssuedAt         string `json:"issued_at"`
	ExpiresAt        string `json:"expires_at"`
	Features         string `json:"features"`
}

type generateLicenseResponse struct {
	License         SignedLicense `json:"license"`
	LicenseJSON     string        `json:"license_json"`
	PublicKeyBase64 string        `json:"public_key_base64"`
	PayloadJSON     string        `json:"payload_json"`
}

type parseRequestResponse struct {
	Request ActivationRequest `json:"request"`
}

var pageTmpl = template.Must(template.New("index").Parse(indexHTML))

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", handleIndex)
	mux.HandleFunc("/api/health", handleHealth)
	mux.HandleFunc("/api/keypair", handleKeypair)
	mux.HandleFunc("/api/request/parse", handleRequestParse)
	mux.HandleFunc("/api/license/generate", handleGenerateLicense)

	addr := envOrDefault("ACTIVATOR_LISTEN_ADDR", ":8090")
	log.Printf("License activator %s listening on http://localhost%s", appVersion, addr)
	if err := http.ListenAndServe(addr, loggingMiddleware(mux)); err != nil {
		log.Fatal(err)
	}
}

func handleIndex(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}
	data := struct {
		Version              string
		DefaultProduct       string
		EnvPrivateKeyPresent bool
	}{
		Version:              appVersion,
		DefaultProduct:       envOrDefault("ACTIVATOR_DEFAULT_PRODUCT", "force_monitor"),
		EnvPrivateKeyPresent: strings.TrimSpace(os.Getenv("LICENSE_PRIVATE_KEY_BASE64")) != "",
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	if err := pageTmpl.Execute(w, data); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func handleHealth(w http.ResponseWriter, r *http.Request) {
	if !allowMethod(w, r, http.MethodGet) {
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"ok":      true,
		"version": appVersion,
		"time":    time.Now().UTC().Format(time.RFC3339),
	})
}

func handleKeypair(w http.ResponseWriter, r *http.Request) {
	if !allowMethod(w, r, http.MethodPost) {
		return
	}
	pub, priv, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		writeError(w, http.StatusInternalServerError, err)
		return
	}
	writeJSON(w, http.StatusOK, keyPairResponse{
		PublicKeyBase64:  base64.StdEncoding.EncodeToString(pub),
		PrivateKeyBase64: base64.StdEncoding.EncodeToString(priv),
	})
}

func handleRequestParse(w http.ResponseWriter, r *http.Request) {
	if !allowMethod(w, r, http.MethodPost) {
		return
	}
	var body struct {
		Text string `json:"text"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		writeError(w, http.StatusBadRequest, fmt.Errorf("invalid json: %w", err))
		return
	}
	var req ActivationRequest
	if err := json.Unmarshal([]byte(strings.TrimSpace(body.Text)), &req); err != nil {
		writeError(w, http.StatusBadRequest, fmt.Errorf("invalid activation request json: %w", err))
		return
	}
	writeJSON(w, http.StatusOK, parseRequestResponse{Request: req})
}

func handleGenerateLicense(w http.ResponseWriter, r *http.Request) {
	if !allowMethod(w, r, http.MethodPost) {
		return
	}
	var req generateLicenseRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, fmt.Errorf("invalid json: %w", err))
		return
	}

	privateKeyBase64 := strings.TrimSpace(req.PrivateKeyBase64)
	if privateKeyBase64 == "" {
		privateKeyBase64 = strings.TrimSpace(os.Getenv("LICENSE_PRIVATE_KEY_BASE64"))
	}
	if privateKeyBase64 == "" {
		writeError(w, http.StatusBadRequest, errors.New("private_key_base64 is required or set LICENSE_PRIVATE_KEY_BASE64"))
		return
	}

	lic, payloadJSON, pubKey, err := buildSignedLicense(req, privateKeyBase64)
	if err != nil {
		writeError(w, http.StatusBadRequest, err)
		return
	}

	b, err := json.MarshalIndent(lic, "", "  ")
	if err != nil {
		writeError(w, http.StatusInternalServerError, err)
		return
	}

	writeJSON(w, http.StatusOK, generateLicenseResponse{
		License:         lic,
		LicenseJSON:     string(b),
		PublicKeyBase64: pubKey,
		PayloadJSON:     payloadJSON,
	})
}

func buildSignedLicense(req generateLicenseRequest, privateKeyBase64 string) (SignedLicense, string, string, error) {
	app := strings.TrimSpace(req.App)
	if app == "" {
		app = envOrDefault("ACTIVATOR_DEFAULT_PRODUCT", "force_monitor")
	}
	lic := SignedLicense{
		App:         app,
		LicenseID:   strings.TrimSpace(req.LicenseID),
		Customer:    strings.TrimSpace(req.Customer),
		Fingerprint: normalizeFingerprint(req.Fingerprint),
		IssuedAt:    strings.TrimSpace(req.IssuedAt),
		ExpiresAt:   strings.TrimSpace(req.ExpiresAt),
		Features:    parseFeatures(req.Features),
	}

	if lic.LicenseID == "" {
		lic.LicenseID = "LIC-" + time.Now().UTC().Format("20060102-150405")
	}
	if lic.Fingerprint == "" {
		return SignedLicense{}, "", "", errors.New("fingerprint is required")
	}
	if lic.IssuedAt == "" {
		lic.IssuedAt = time.Now().UTC().Format(time.RFC3339)
	}
	if _, err := time.Parse(time.RFC3339, lic.IssuedAt); err != nil {
		return SignedLicense{}, "", "", fmt.Errorf("issued_at must be RFC3339, example 2026-04-20T12:00:00Z")
	}
	if lic.ExpiresAt != "" {
		if _, err := time.Parse(time.RFC3339, lic.ExpiresAt); err != nil {
			return SignedLicense{}, "", "", fmt.Errorf("expires_at must be RFC3339, example 2027-04-20T00:00:00Z")
		}
	}

	privRaw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(privateKeyBase64))
	if err != nil {
		return SignedLicense{}, "", "", fmt.Errorf("decode private key: %w", err)
	}
	if len(privRaw) != ed25519.PrivateKeySize {
		return SignedLicense{}, "", "", fmt.Errorf("invalid private key size: got %d want %d", len(privRaw), ed25519.PrivateKeySize)
	}
	priv := ed25519.PrivateKey(privRaw)

	payload := licensePayload{
		App:         lic.App,
		LicenseID:   lic.LicenseID,
		Customer:    lic.Customer,
		Fingerprint: lic.Fingerprint,
		IssuedAt:    lic.IssuedAt,
		ExpiresAt:   lic.ExpiresAt,
		Features:    lic.Features,
	}
	payloadBytes, err := json.Marshal(payload)
	if err != nil {
		return SignedLicense{}, "", "", fmt.Errorf("marshal payload: %w", err)
	}

	sig := ed25519.Sign(priv, payloadBytes)
	lic.Signature = base64.StdEncoding.EncodeToString(sig)
	pub := priv.Public().(ed25519.PublicKey)
	return lic, string(payloadBytes), base64.StdEncoding.EncodeToString(pub), nil
}

func parseFeatures(raw string) []string {
	parts := strings.Split(raw, ",")
	out := make([]string, 0, len(parts))
	seen := map[string]struct{}{}
	for _, p := range parts {
		v := strings.TrimSpace(p)
		if v == "" {
			continue
		}
		if _, ok := seen[v]; ok {
			continue
		}
		seen[v] = struct{}{}
		out = append(out, v)
	}
	return out
}

func normalizeFingerprint(v string) string {
	v = strings.TrimSpace(strings.ToUpper(v))
	v = strings.ReplaceAll(v, "-", "")
	v = strings.ReplaceAll(v, " ", "")
	return v
}

func writeJSON(w http.ResponseWriter, status int, v any) {
	w.Header().Set("Content-Type", "application/json")
	w.Header().Set("Cache-Control", "no-store")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(v)
}

func writeError(w http.ResponseWriter, status int, err error) {
	writeJSON(w, status, map[string]string{"error": err.Error()})
}

func allowMethod(w http.ResponseWriter, r *http.Request, method string) bool {
	if r.Method == http.MethodOptions {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
		w.WriteHeader(http.StatusNoContent)
		return false
	}
	if r.Method != method {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return false
	}
	return true
}

func envOrDefault(key, def string) string {
	if v := strings.TrimSpace(os.Getenv(key)); v != "" {
		return v
	}
	return def
}

func loggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		next.ServeHTTP(w, r)
		log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start).Round(time.Millisecond))
	})
}

const indexHTML = `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>License Activator</title>
  <style>
    :root {
      --bg: #0b1020;
      --panel: #141b2d;
      --panel2: #1a2339;
      --text: #edf2ff;
      --muted: #9fb0d1;
      --line: #2b3755;
      --accent: #5aa9ff;
      --ok: #36c275;
      --warn: #f2b94b;
    }
    * { box-sizing: border-box; }
    body { margin: 0; font-family: Segoe UI, Arial, sans-serif; background: linear-gradient(180deg, #08101f, #0d1526); color: var(--text); }
    .wrap { max-width: 1360px; margin: 0 auto; padding: 24px; }
    .hero { display:flex; justify-content:space-between; align-items:flex-start; gap:20px; margin-bottom:20px; }
    .tag { display:inline-block; padding:6px 10px; border:1px solid var(--line); border-radius:999px; color: var(--muted); font-size:12px; }
    .grid { display:grid; grid-template-columns: 1.2fr 1fr; gap:20px; }
    .card { background: rgba(20,27,45,0.92); border:1px solid var(--line); border-radius:18px; padding:18px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); }
    h1,h2,h3 { margin:0 0 10px 0; }
    h1 { font-size: 34px; }
    h2 { font-size: 21px; }
    p { color: var(--muted); }
    label { display:block; font-size:13px; color: var(--muted); margin: 0 0 6px 2px; }
    input, textarea, button { width:100%; border-radius:12px; border:1px solid var(--line); background: #0d1526; color: var(--text); padding:12px 14px; }
    textarea { min-height: 120px; resize: vertical; }
    button { background: linear-gradient(180deg, #2579ff, #1f67d8); font-weight:700; cursor:pointer; }
    button.secondary { background: #1a2339; }
    button.ghost { background: transparent; }
    .row { display:grid; grid-template-columns: 1fr 1fr; gap:12px; }
    .row3 { display:grid; grid-template-columns: 1fr 1fr 1fr; gap:12px; }
    .stack { display:flex; flex-direction:column; gap:14px; }
    .toolbar { display:flex; gap:10px; flex-wrap:wrap; }
    .toolbar button { width:auto; min-width:170px; }
    .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
    .out { min-height:220px; }
    .small { font-size:12px; color: var(--muted); }
    .notice { padding:12px 14px; border-radius:14px; border:1px solid var(--line); background:#10192d; color:var(--muted); }
    .ok { color: var(--ok); }
    .warn { color: var(--warn); }
    .top-note { margin-top:8px; }
    @media (max-width: 1000px) { .grid, .row, .row3 { grid-template-columns: 1fr; } .hero { flex-direction:column; } }
  </style>
</head>
<body>
  <div class="wrap">
    <div class="hero">
      <div>
        <div class="tag">License Activator v{{.Version}}</div>
        <h1>Offline License Generator</h1>
        <p>Create Ed25519 keypairs, parse activation requests from the machine, and generate signed license JSON for your protected Go app.</p>
      </div>
      <div class="card" style="min-width:320px; max-width:420px;">
        <h3>Important</h3>
        <p class="small">Keep this activator private. Do not ship your private signing key with the customer machine. Only the public key belongs in the protected app config.</p>
        <div class="small top-note">Env private key loaded: <span class="{{if .EnvPrivateKeyPresent}}ok{{else}}warn{{end}}">{{if .EnvPrivateKeyPresent}}yes{{else}}no{{end}}</span></div>
        <div class="small">Default product code: <span class="mono">{{.DefaultProduct}}</span></div>
      </div>
    </div>

    <div class="grid">
      <div class="stack">
        <div class="card stack">
          <h2>1. Activation request from machine</h2>
          <label for="requestJson">Paste machine activation request JSON</label>
          <textarea id="requestJson" class="mono" placeholder='{"app":"force_monitor","fingerprint":"ABC..."}'></textarea>
          <div class="toolbar">
            <button type="button" class="secondary" id="parseRequestBtn">Parse request into form</button>
            <button type="button" class="ghost" id="clearRequestBtn">Clear request</button>
          </div>
          <div id="requestStatus" class="notice small">Paste the JSON from <span class="mono">/api/license/request</span> of the protected machine, then parse it.</div>
        </div>

        <div class="card stack">
          <h2>2. License data</h2>
          <div class="row3">
            <div>
              <label for="app">App / product code</label>
              <input id="app" value="{{.DefaultProduct}}">
            </div>
            <div>
              <label for="licenseId">License ID</label>
              <input id="licenseId" placeholder="LIC-20260420-0001">
            </div>
            <div>
              <label for="customer">Customer</label>
              <input id="customer" placeholder="IMCO">
            </div>
          </div>
          <div>
            <label for="fingerprint">Machine fingerprint</label>
            <input id="fingerprint" class="mono" placeholder="paste or auto-fill from request">
          </div>
          <div class="row">
            <div>
              <label for="issuedAt">Issued at (RFC3339)</label>
              <input id="issuedAt" class="mono" placeholder="2026-04-20T12:00:00Z">
            </div>
            <div>
              <label for="expiresAt">Expires at (optional RFC3339)</label>
              <input id="expiresAt" class="mono" placeholder="2027-04-20T00:00:00Z">
            </div>
          </div>
          <div>
            <label for="features">Features (comma separated)</label>
            <input id="features" placeholder="core,mqtt">
          </div>
        </div>

        <div class="card stack">
          <h2>3. Signing key</h2>
          <label for="privateKey">Private key base64</label>
          <textarea id="privateKey" class="mono" placeholder="paste Ed25519 private key base64 here or use environment variable LICENSE_PRIVATE_KEY_BASE64"></textarea>
          <div class="toolbar">
            <button type="button" class="secondary" id="generateKeypairBtn">Generate new keypair</button>
            <button type="button" id="generateLicenseBtn">Generate signed license</button>
          </div>
          <div class="small">The generated public key must be copied into the protected app config as <span class="mono">license.public_key_base64</span>.</div>
        </div>
      </div>

      <div class="stack">
        <div class="card stack">
          <h2>Generated keypair</h2>
          <label for="publicKeyOut">Public key base64</label>
          <textarea id="publicKeyOut" class="mono out" readonly></textarea>
          <label for="privateKeyOut">Private key base64</label>
          <textarea id="privateKeyOut" class="mono out" readonly></textarea>
        </div>

        <div class="card stack">
          <h2>Signed license JSON</h2>
          <label for="payloadOut">Canonical payload used for signing</label>
          <textarea id="payloadOut" class="mono out" readonly></textarea>
          <label for="licenseOut">License JSON to copy into the machine</label>
          <textarea id="licenseOut" class="mono out" readonly></textarea>
          <div id="licenseStatus" class="notice small">Generate a license and copy the JSON into the protected machine activation page.</div>
        </div>
      </div>
    </div>
  </div>

<script>
async function postJSON(url, body) {
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body)
  });
  const text = await res.text();
  let data = null;
  try { data = JSON.parse(text); } catch (_) {}
  if (!res.ok) {
    throw new Error(data && data.error ? data.error : text || ('HTTP ' + res.status));
  }
  return data;
}

function setNowIfEmpty() {
  const el = document.getElementById('issuedAt');
  if (el && !el.value.trim()) {
    el.value = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
  }
}

function setStatus(id, text) {
  const el = document.getElementById(id);
  if (el) el.textContent = text;
}

function fillFromRequest(req) {
  if (!req) return;
  if (req.app) document.getElementById('app').value = req.app;
  if (req.fingerprint) document.getElementById('fingerprint').value = req.fingerprint;
  setStatus('requestStatus', 'Loaded request for ' + (req.hostname || 'unknown host') + ' • fingerprint ' + (req.fingerprint_short || req.fingerprint || '')); 
}

document.getElementById('parseRequestBtn').addEventListener('click', async () => {
  try {
    const text = document.getElementById('requestJson').value.trim();
    const data = await postJSON('/api/request/parse', { text });
    fillFromRequest(data.request);
  } catch (err) {
    setStatus('requestStatus', err.message);
  }
});

document.getElementById('clearRequestBtn').addEventListener('click', () => {
  document.getElementById('requestJson').value = '';
  setStatus('requestStatus', 'Request cleared.');
});

document.getElementById('generateKeypairBtn').addEventListener('click', async () => {
  try {
    const data = await postJSON('/api/keypair', {});
    document.getElementById('publicKeyOut').value = data.public_key_base64 || '';
    document.getElementById('privateKeyOut').value = data.private_key_base64 || '';
    document.getElementById('privateKey').value = data.private_key_base64 || '';
    setStatus('licenseStatus', 'New keypair generated. Put the public key into the protected app config. Keep the private key secret.');
  } catch (err) {
    setStatus('licenseStatus', err.message);
  }
});

document.getElementById('generateLicenseBtn').addEventListener('click', async () => {
  try {
    setNowIfEmpty();
    const body = {
      private_key_base64: document.getElementById('privateKey').value,
      app: document.getElementById('app').value,
      license_id: document.getElementById('licenseId').value,
      customer: document.getElementById('customer').value,
      fingerprint: document.getElementById('fingerprint').value,
      issued_at: document.getElementById('issuedAt').value,
      expires_at: document.getElementById('expiresAt').value,
      features: document.getElementById('features').value,
    };
    const data = await postJSON('/api/license/generate', body);
    document.getElementById('payloadOut').value = data.payload_json || '';
    document.getElementById('licenseOut').value = data.license_json || '';
    if (data.public_key_base64) document.getElementById('publicKeyOut').value = data.public_key_base64;
    setStatus('licenseStatus', 'License generated successfully. Copy the JSON into the target machine activation page or save it as license.json.');
  } catch (err) {
    setStatus('licenseStatus', err.message);
  }
});

setNowIfEmpty();
</script>
</body>
</html>`
