package main

// Page de réglages locale (http://127.0.0.1:47821) : dossier des logs, canaux, serveur + jeton, test, import de l'historique.
// N'écoute que sur 127.0.0.1 : rien n'est exposé au réseau.

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"sort"
	"strings"
	"time"
)

func (a *agent) startUI() string {
	var ln net.Listener
	var err error
	for port := 47821; port < 47831; port++ {
		if ln, err = net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port)); err == nil {
			break
		}
	}
	if ln == nil {
		warn("page de réglages indisponible : %v", err)
		return ""
	}
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		io.WriteString(w, uiHTML)
	})
	mux.HandleFunc("/api/state", func(w http.ResponseWriter, r *http.Request) {
		a.mu.Lock()
		st := map[string]any{"version": Version, "cfg": a.cfg, "path": a.cfgPath, "logs_dir": a.logsDir(), "default_dir": defaultLogsDir(),
			"sending": a.send, "last_send": unix(a.lastSend), "last_err": a.lastErr, "m_last": unix(a.mLast), "m_err": a.mErr, "following": a.following, "importing": a.importing}
		a.mu.Unlock()
		writeJSON(w, st)
	})
	mux.HandleFunc("/api/scan", func(w http.ResponseWriter, r *http.Request) {
		dir := strings.TrimSpace(r.URL.Query().Get("dir"))
		if dir == "" {
			dir = defaultLogsDir()
		}
		a.mu.Lock()
		ln := a.cfg.LocalNames
		a.mu.Unlock()
		writeJSON(w, scanChannels(dir, ln))
	})
	mux.HandleFunc("/api/browse", func(w http.ResponseWriter, r *http.Request) {
		p, err := browseFolder(r.URL.Query().Get("dir"))
		if err != nil {
			writeJSON(w, map[string]any{"error": err.Error()})
			return
		}
		writeJSON(w, map[string]any{"path": p})
	})
	mux.HandleFunc("/api/test", func(w http.ResponseWriter, r *http.Request) {
		var c Config
		if json.NewDecoder(r.Body).Decode(&c) != nil {
			writeJSON(w, map[string]any{"error": "requête invalide"})
			return
		}
		t := &agent{cfg: &c, client: &http.Client{Timeout: 10 * time.Second}}
		res, err := t.post(Payload{V: Version, Lines: []Line{}, Locs: []Loc{}})
		if err != nil {
			writeJSON(w, map[string]any{"error": err.Error()})
			return
		}
		writeJSON(w, map[string]any{"ok": res.OK})
	})
	mux.HandleFunc("/api/testmap", func(w http.ResponseWriter, r *http.Request) {
		var c Config
		if json.NewDecoder(r.Body).Decode(&c) != nil || c.MapperURL == "" {
			writeJSON(w, map[string]any{"error": "URL du mapper manquante"})
			return
		}
		t := &agent{cfg: &c, client: &http.Client{Timeout: 10 * time.Second}}
		res, err := t.postTo(c.MapperURL, c.MapperToken, Payload{V: Version, Lines: []Line{}, Locs: []Loc{}})
		if err != nil {
			writeJSON(w, map[string]any{"error": err.Error()})
			return
		}
		writeJSON(w, map[string]any{"ok": res.OK})
	})
	mux.HandleFunc("/api/save", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != "POST" {
			http.Error(w, "POST", 405)
			return
		}
		var c Config
		if json.NewDecoder(r.Body).Decode(&c) != nil {
			writeJSON(w, map[string]any{"error": "requête invalide"})
			return
		}
		a.mu.Lock()
		cur := *a.cfg
		a.mu.Unlock()
		cur.LogsDir, cur.Channels, cur.Local, cur.APIURL, cur.Token = strings.TrimSpace(c.LogsDir), c.Channels, c.Local, strings.TrimSpace(c.APIURL), strings.TrimSpace(c.Token)
		cur.MapperURL, cur.MapperToken = strings.TrimSpace(c.MapperURL), strings.TrimSpace(c.MapperToken)
		if err := saveConfig(a.cfgPath, &cur); err != nil {
			writeJSON(w, map[string]any{"error": err.Error()})
			return
		}
		select {
		case <-a.reload:
		default:
		}
		a.reload <- &cur
		writeJSON(w, map[string]any{"ok": true})
	})
	mux.HandleFunc("/api/import", func(w http.ResponseWriter, r *http.Request) {
		a.mu.Lock()
		busy := a.importing != "" && !strings.HasPrefix(a.importing, "terminé")
		if !busy {
			a.importing = "démarrage…"
		}
		a.mu.Unlock()
		if busy {
			writeJSON(w, map[string]any{"error": "import déjà en cours"})
			return
		}
		a.mu.Lock()
		c := *a.cfg
		a.mu.Unlock()
		go func() {
			b := &agent{cfg: &c, cfgPath: a.cfgPath, tails: map[string]*tail{}, client: &http.Client{Timeout: 60 * time.Second}, status: a}
			b.applyConfig(&c)
			b.cfg.Quiet = true
			b.importAll()
			b.flush(true)
			a.mu.Lock()
			a.importing = fmt.Sprintf("terminé : %d lignes envoyées", b.sent)
			a.mu.Unlock()
		}()
		writeJSON(w, map[string]any{"ok": true})
	})
	go http.Serve(ln, mux)
	return "http://" + ln.Addr().String() + "/"
}

func unix(t time.Time) int64 {
	if t.IsZero() {
		return 0
	}
	return t.Unix()
}

func writeJSON(w http.ResponseWriter, v any) {
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(v)
}

type chanInfo struct {
	Name  string `json:"name"`
	Files int    `json:"files"`
	Last  int64  `json:"last"`
	Local bool   `json:"local"`
	Intel bool   `json:"intel"` // ressemble à un canal d'intel
}

// Canaux présents dans le dossier (30 derniers jours), du plus récent au plus ancien.
func scanChannels(dir string, localNames []string) map[string]any {
	ents, err := os.ReadDir(dir)
	if err != nil {
		return map[string]any{"dir": dir, "error": "dossier introuvable ou illisible"}
	}
	m := map[string]*chanInfo{}
	limit := time.Now().Add(-30 * 24 * time.Hour)
	total := 0
	for _, e := range ents {
		f := reFile.FindStringSubmatch(e.Name())
		if f == nil || e.IsDir() {
			continue
		}
		fi, err := e.Info()
		if err != nil || fi.ModTime().Before(limit) {
			continue
		}
		total++
		c := m[f[1]]
		if c == nil {
			c = &chanInfo{Name: f[1]}
			for _, l := range localNames {
				if strings.EqualFold(l, f[1]) {
					c.Local = true
				}
			}
			low := strings.ToLower(f[1])
			c.Intel = strings.Contains(low, "intel") || strings.HasPrefix(low, "i. ")
			m[f[1]] = c
		}
		c.Files++
		if t := fi.ModTime().Unix(); t > c.Last {
			c.Last = t
		}
	}
	out := make([]*chanInfo, 0, len(m))
	for _, c := range m {
		out = append(out, c)
	}
	sort.Slice(out, func(i, j int) bool {
		if out[i].Intel != out[j].Intel {
			return out[i].Intel
		}
		return out[i].Last > out[j].Last
	})
	return map[string]any{"dir": dir, "channels": out, "files": total}
}

// Fenêtre Windows « Parcourir » (via PowerShell, rien à installer).
func browseFolder(start string) (string, error) {
	if runtime.GOOS != "windows" {
		return "", fmt.Errorf("« Parcourir » n'existe que sous Windows : tape le chemin")
	}
	if start == "" || !dirOK(start) {
		start = filepath.Dir(defaultLogsDir())
	}
	ps := `[Console]::OutputEncoding=[Text.Encoding]::UTF8; Add-Type -AssemblyName System.Windows.Forms;` +
		`$f=New-Object System.Windows.Forms.Form -Property @{TopMost=$true;ShowInTaskbar=$false;WindowState='Minimized'};` +
		`$d=New-Object System.Windows.Forms.FolderBrowserDialog; $d.Description='Dossier Chatlogs d''EVE'; $d.SelectedPath=$env:EON_START;` +
		`if($d.ShowDialog($f) -eq 'OK'){[Console]::Out.Write($d.SelectedPath)}`
	cmd := exec.Command("powershell", "-NoProfile", "-STA", "-ExecutionPolicy", "Bypass", "-Command", ps)
	cmd.Env = append(os.Environ(), "EON_START="+start)
	hideWindow(cmd)
	var out bytes.Buffer
	cmd.Stdout = &out
	if err := cmd.Run(); err != nil {
		return "", fmt.Errorf("fenêtre de choix impossible (%v) : tape le chemin", err)
	}
	return strings.TrimSpace(out.String()), nil
}

func openBrowser(url string) {
	var cmd *exec.Cmd
	switch runtime.GOOS {
	case "windows":
		cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
	case "darwin":
		cmd = exec.Command("open", url)
	default:
		cmd = exec.Command("xdg-open", url)
	}
	_ = cmd.Start()
}

const uiHTML = `<!doctype html><html lang="fr"><head><meta charset="utf-8"><title>EON Intel — agent</title>
<style>
:root{--bg:#0e1116;--panel:#161b22;--line:#2a313c;--txt:#d6dbe1;--mut:#8b95a3;--acc:#e0a33a;--ok:#4fb37a;--bad:#e05a4f}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--txt);font:14px/1.5 system-ui,sans-serif}
main{max-width:760px;margin:auto;padding:18px}h1{font-size:18px;color:var(--acc);letter-spacing:.06em;margin:0 0 4px}h2{font-size:15px;margin:22px 0 8px}
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px}
input[type=text],input[type=password]{width:100%;background:var(--bg);color:var(--txt);border:1px solid var(--line);border-radius:6px;padding:7px 9px;font:inherit}
button{background:transparent;color:var(--acc);border:1px solid var(--acc);border-radius:6px;padding:6px 12px;font:inherit;cursor:pointer}button.primary{background:var(--acc);color:#141414;font-weight:600}
.row{display:flex;gap:8px;align-items:center}.row input{flex:1}.mut{color:var(--mut)}.small{font-size:12.5px}.ok{color:var(--ok)}.bad{color:var(--bad)}
label.ch{display:flex;gap:8px;align-items:center;padding:5px 6px;border-bottom:1px solid var(--line);cursor:pointer}label.ch:hover{background:#1b212a}label.ch span.m{margin-left:auto;color:var(--mut);font-size:12px}
.st{display:grid;grid-template-columns:max-content 1fr;gap:3px 14px;font-size:13px}.st dt{color:var(--mut)}.st dd{margin:0;word-break:break-all}
.drop{border:1px dashed var(--line);border-radius:8px;padding:10px;text-align:center;color:var(--mut);margin-bottom:10px;cursor:pointer}.drop.hov{border-color:var(--acc);color:var(--acc)}
</style></head><body><main>
<h1>EON INTEL · agent</h1><div class="mut small" id="ver"></div>

<h2>1 · Serveur et jeton</h2>
<div class="card">
  <div class="drop" id="drop">Glisse ici le fichier <b>intel-agent.json</b> téléchargé depuis Intel → Réglages → Agents (ou clique pour le choisir)<input type="file" id="file" accept=".json" hidden></div>
  <div class="small mut">URL de l'API</div><input type="text" id="api" placeholder="https://ddbuiq.tech/eon/intel/api.php">
  <div class="small mut" style="margin-top:8px">Jeton</div><div class="row"><input type="password" id="tok" placeholder="eon_…"><button id="show">voir</button><button id="test">Tester</button></div>
  <div id="testres" class="small"></div>
  <div class="small mut" style="margin-top:12px">EON Mapper (facultatif) : URL de l'API du mapper, reçoit seulement les positions de tes persos</div><input type="text" id="murl" placeholder="https://ddbuiq.tech/eon/mapper/api.php">
  <div class="small mut" style="margin-top:8px">Jeton du mapper</div><div class="row"><input type="password" id="mtok" placeholder="Mapper → Settings → Agents"><button id="testm">Tester</button></div>
  <div id="testmres" class="small"></div>
</div>

<h2>2 · Dossier des logs EVE</h2>
<div class="card">
  <div class="row"><input type="text" id="dir"><button id="browse">Parcourir…</button></div>
  <div class="small mut" id="dirinfo"></div>
</div>

<h2>3 · Canaux à suivre</h2>
<div class="card">
  <div class="small mut">Canaux trouvés dans le dossier (30 derniers jours). Coche tes canaux d'intel : l'agent suit toujours le fichier le plus récent de chacun.</div>
  <div id="chans"></div>
  <label class="ch" style="border:0;margin-top:6px"><input type="checkbox" id="local"> Envoyer la position de mes persos (logs Local : seulement la ligne « Channel changed to Local », jamais les messages)</label>
</div>

<p class="row"><button class="primary" id="save">Enregistrer et lancer</button><span id="saved" class="small"></span></p>

<h2>État</h2>
<div class="card"><dl class="st" id="state"></dl>
  <p class="row" style="margin:10px 0 0"><button id="imp">Importer l'historique</button><span class="small mut">envoie tous les anciens fichiers des canaux cochés (doublons ignorés par le serveur)</span></p></div>
</main>
<script>
const $=s=>document.querySelector(s);
let S=null, chans=[];
const ago=t=>{if(!t)return 'jamais';const s=Math.round(Date.now()/1000-t);return s<60?'il y a '+s+' s':s<3600?'il y a '+Math.floor(s/60)+' min':s<86400?'il y a '+Math.floor(s/3600)+' h':'il y a '+Math.floor(s/86400)+' j'};
const esc=s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
async function j(u,o){const r=await fetch(u,o);return r.json()}
async function load(){
  S=await j('/api/state'); const c=S.cfg;
  $('#ver').textContent='version '+S.version+' · config : '+S.path;
  $('#api').value=c.api_url||''; $('#tok').value=c.token||''; $('#murl').value=c.mapper_url||''; $('#mtok').value=c.mapper_token||''; $('#dir').value=c.logs_dir||''; $('#dir').placeholder=S.default_dir+' (par défaut)';
  $('#local').checked=!!c.local;
  await scan(); state();
}
async function scan(){
  const r=await j('/api/scan?dir='+encodeURIComponent($('#dir').value.trim()));
  if(r.error){$('#dirinfo').innerHTML='<span class="bad">'+esc(r.dir)+' : '+r.error+'</span>';chans=[];$('#chans').innerHTML='';return}
  $('#dirinfo').innerHTML='<span class="ok">✓</span> '+esc(r.dir)+' · '+r.files+' fichiers récents';
  const sel=new Set((S.cfg.channels||[]).map(x=>x.toLowerCase()));
  chans=r.channels.filter(c=>!c.local);
  $('#chans').innerHTML=chans.map((c,i)=>'<label class="ch"><input type="checkbox" data-i="'+i+'" '+(sel.has(c.name.toLowerCase())?'checked':'')+'> <b>'+esc(c.name)+'</b>'+(c.intel?'':' <span class="mut small">(pas un canal d\'intel ?)</span>')+'<span class="m">'+c.files+' fichier'+(c.files>1?'s':'')+' · '+ago(c.last)+'</span></label>').join('')||'<div class="mut">Aucun fichier de chat récent dans ce dossier.</div>';
  for(const n of S.cfg.channels||[]) if(!chans.some(c=>c.name.toLowerCase()===n.toLowerCase())) $('#chans').insertAdjacentHTML('beforeend','<label class="ch"><input type="checkbox" data-n="'+esc(n)+'" checked> <b>'+esc(n)+'</b><span class="m">pas de fichier récent</span></label>');
}
function cfg(){
  const ch=[...document.querySelectorAll('#chans input:checked')].map(x=>x.dataset.n||chans[+x.dataset.i].name);
  return {logs_dir:$('#dir').value.trim(),channels:ch,local:$('#local').checked,api_url:$('#api').value.trim(),token:$('#tok').value.trim(),mapper_url:$('#murl').value.trim(),mapper_token:$('#mtok').value.trim()};
}
async function state(){
  S=await j('/api/state');
  $('#state').innerHTML='<dt>envoi</dt><dd>'+(S.sending?'<span class="ok">actif</span> → '+esc(S.cfg.api_url):'<span class="bad">non</span> (URL ou jeton manquant)')+'</dd>'
   +'<dt>dernier envoi</dt><dd>'+ago(S.last_send)+'</dd>'
   +(S.cfg.mapper_url?'<dt>mapper</dt><dd>'+(S.m_err?'<span class="bad">'+esc(S.m_err)+'</span>':'positions envoyées '+ago(S.m_last))+'</dd>':'')
   +(S.last_err?'<dt>erreur</dt><dd class="bad">'+esc(S.last_err)+'</dd>':'')
   +'<dt>fichiers suivis</dt><dd>'+((S.following||[]).map(esc).join('<br>')||'aucun')+'</dd>'
   +(S.importing?'<dt>import</dt><dd>'+esc(S.importing)+'</dd>':'');
}
$('#dir').addEventListener('change',scan);
$('#browse').onclick=async()=>{const r=await j('/api/browse?dir='+encodeURIComponent($('#dir').value.trim()||S.default_dir));if(r.error){alert(r.error);return}if(r.path){$('#dir').value=r.path;scan()}};
$('#show').onclick=()=>{const t=$('#tok');t.type=t.type==='password'?'text':'password'};
$('#test').onclick=async()=>{$('#testres').textContent='…';const r=await j('/api/test',{method:'POST',body:JSON.stringify(cfg())});$('#testres').innerHTML=r.ok?'<span class="ok">✓ le serveur accepte ce jeton</span>':'<span class="bad">✗ '+esc(r.error)+'</span>'};
$('#save').onclick=async()=>{const c=cfg();if(!c.channels.length&&!confirm('Aucun canal coché : continuer ?'))return;const r=await j('/api/save',{method:'POST',body:JSON.stringify(c)});$('#saved').innerHTML=r.ok?'<span class="ok">✓ enregistré, l\'agent repart avec cette config</span>':'<span class="bad">'+esc(r.error)+'</span>';setTimeout(load,1500)};
$('#testm').onclick=async()=>{$('#testmres').textContent='…';const r=await j('/api/testmap',{method:'POST',body:JSON.stringify(cfg())});$('#testmres').innerHTML=r.ok?'<span class="ok">✓ le mapper accepte ce jeton</span>':'<span class="bad">✗ '+esc(r.error)+'</span>'};
$('#imp').onclick=async()=>{const r=await j('/api/import',{method:'POST'});if(r.error)alert(r.error);state()};
function readFile(f){const rd=new FileReader();rd.onload=()=>{try{const c=JSON.parse(rd.result.replace(/^\uFEFF/,''));if(c.api_url)$('#api').value=c.api_url;if(c.token)$('#tok').value=c.token;if(c.mapper_url)$('#murl').value=c.mapper_url;if(c.mapper_token)$('#mtok').value=c.mapper_token;$('#testres').innerHTML='<span class="ok">fichier lu : URL et jeton remplis. Clique « Tester » puis « Enregistrer ».</span>'}catch(e){alert('Fichier illisible : '+e.message)}};rd.readAsText(f)}
const dr=$('#drop');dr.onclick=()=>$('#file').click();$('#file').onchange=e=>e.target.files[0]&&readFile(e.target.files[0]);
dr.ondragover=e=>{e.preventDefault();dr.classList.add('hov')};dr.ondragleave=()=>dr.classList.remove('hov');dr.ondrop=e=>{e.preventDefault();dr.classList.remove('hov');e.dataTransfer.files[0]&&readFile(e.dataTransfer.files[0])};
load();setInterval(state,3000);
</script></body></html>`
