//go:build js && wasm

package main

import (
	"net/url"
	"regexp"
	"strings"
	"syscall/js"

	"e-li.nps/internal/contratos"
)

// IMPORTANTE (.agent): o backend Go continua sendo a autoridade das regras.
// Este WASM existe para concentrar regras de negócio do widget (cliente) em Go,
// mantendo o arquivo JS pequeno e fácil de auditar.

var dataISORe = regexp.MustCompile(`^([0-9]{4})-([0-9]{2})-([0-9]{2})$`)

type cfgWidget = contratos.ConfigWidget

func main() {
	js.Global().Set("__eli_nps_wasm_preflight", js.FuncOf(preflight))
	js.Global().Set("__eli_nps_wasm_decidir", js.FuncOf(decidir))
	js.Global().Set("__eli_nps_wasm_cooldown_ativo", js.FuncOf(cooldownAtivo))
	js.Global().Set("__eli_nps_wasm_set_cooldown", js.FuncOf(setCooldown))
	js.Global().Set("__eli_nps_wasm_painel_init", js.FuncOf(painelInit))
	js.Global().Set("__eli_nps_wasm_ready", true)

	// Mantém o módulo vivo.
	select {}
}

func painelInit(this js.Value, args []js.Value) any {
	// Painel: persistência de filtros no localStorage.
	// Regras (.agent): lógica no WASM (Go). Aqui é best-effort e fail-open.
	//
	// - Salva: produto selecionado + checkbox "baixas".
	// - Restaura: se a URL não tiver parâmetros e houver valor salvo,
	//   redireciona para /painel?produto=...&baixas=1.
	//
	// Importante: não roda fora do /painel.
	loc := js.Global().Get("location")
	if loc.IsUndefined() || loc.IsNull() {
		return nil
	}
	path := loc.Get("pathname").String()
	if path == "" || !strings.HasPrefix(path, "/painel") {
		return nil
	}

	storage := js.Global().Get("localStorage")
	if storage.IsUndefined() || storage.IsNull() {
		return nil
	}

	const keyProd = "eli_nps_painel_produto"
	const keyBaixas = "eli_nps_painel_baixas"

	// Aguarda o DOM estar pronto para conseguirmos acessar o form/option list.
	doc := js.Global().Get("document")
	if doc.IsUndefined() || doc.IsNull() {
		return nil
	}

	handler := js.FuncOf(func(this js.Value, args []js.Value) any {
		// Best-effort: evita exceptions se APIs não existirem.
		defer func() { _ = recover() }()

		form := doc.Call("querySelector", `form[action="/painel"]`)
		if form.IsUndefined() || form.IsNull() {
			return nil
		}

		sel := form.Call("querySelector", `select[name="produto"]`)
		chk := form.Call("querySelector", `input[name="baixas"]`)

		// Helper: verifica se option existe.
		optionExists := func(selectEl js.Value, value string) bool {
			if selectEl.IsUndefined() || selectEl.IsNull() {
				return false
			}
			opts := selectEl.Get("options")
			ln := opts.Get("length").Int()
			for i := 0; i < ln; i++ {
				opt := opts.Index(i)
				if opt.Get("value").String() == value {
					return true
				}
			}
			return false
		}

		persist := func() {
			if !sel.IsUndefined() && !sel.IsNull() {
				v := strings.TrimSpace(sel.Get("value").String())
				if v != "" {
					storage.Call("setItem", keyProd, v)
				}
			}
			baixas := "0"
			if !chk.IsUndefined() && !chk.IsNull() && chk.Get("checked").Truthy() {
				baixas = "1"
			}
			storage.Call("setItem", keyBaixas, baixas)
		}

		// Restaura/redirect apenas se não há query string.
		s := loc.Get("search").String()
		if strings.TrimSpace(s) == "" {
			storedProd := strings.TrimSpace(storage.Call("getItem", keyProd).String())
			storedBaixas := strings.TrimSpace(storage.Call("getItem", keyBaixas).String())
			if storedProd != "" && optionExists(sel, storedProd) {
				q := "?produto=" + url.QueryEscape(storedProd)
				if storedBaixas == "1" {
					q += "&baixas=1"
				}
				loc.Call("replace", path+q)
				return nil
			}
			// Se só baixas estiver setado, aplica também.
			if storedBaixas == "1" {
				loc.Call("replace", path+"?baixas=1")
				return nil
			}
		}

		// Liga listeners para persistência.
		onSubmit := js.FuncOf(func(this js.Value, args []js.Value) any { persist(); return nil })
		onChange := js.FuncOf(func(this js.Value, args []js.Value) any { persist(); return nil })
		form.Call("addEventListener", "submit", onSubmit)
		form.Call("addEventListener", "change", onChange)

		return nil
	})

	// DOMContentLoaded
	readyState := doc.Get("readyState").String()
	if readyState != "loading" {
		// Se o WASM foi carregado depois do DOM pronto (comum no painel),
		// rodamos imediatamente.
		handler.Invoke()
	} else {
		doc.Call("addEventListener", "DOMContentLoaded", handler)
	}
	return nil
}

func cooldownAtivo(this js.Value, args []js.Value) any {
	if len(args) < 1 {
		return false
	}
	chave := strings.TrimSpace(args[0].String())
	if chave == "" {
		return false
	}

	// Best-effort: se der erro, tratamos como não ativo.
	storage := js.Global().Get("localStorage")
	if storage.IsUndefined() || storage.IsNull() {
		return false
	}

	v := storage.Call("getItem", chave)
	if v.IsNull() || v.IsUndefined() {
		return false
	}

	parsed := js.Global().Get("JSON").Call("parse", v)
	until := parsed.Get("until")
	if until.IsUndefined() || until.IsNull() {
		return false
	}

	agora := js.Global().Get("Date").Call("now").Int()
	return int64(agora) < int64(until.Int())
}

func setCooldown(this js.Value, args []js.Value) any {
	if len(args) < 2 {
		return nil
	}
	chave := strings.TrimSpace(args[0].String())
	if chave == "" {
		return nil
	}
	ateMs := int64(args[1].Int())
	if ateMs <= 0 {
		return nil
	}

	storage := js.Global().Get("localStorage")
	if storage.IsUndefined() || storage.IsNull() {
		return nil
	}

	obj := js.Global().Get("Object").New()
	obj.Set("until", ateMs)
	json := js.Global().Get("JSON").Call("stringify", obj)
	storage.Call("setItem", chave, json)
	return nil
}

func preflight(this js.Value, args []js.Value) any {
	if len(args) < 1 {
		return map[string]any{"ok": false, "motivo": "sem_cfg"}
	}
	cfg := lerCfg(args[0])

	// Bloqueio por data mínima.
	if antesDaDataMinima(cfg.DataMinimaAbertura) {
		return map[string]any{"ok": false, "motivo": "antes_da_data_minima"}
	}

	// controle de exibição: produto + inquilino_codigo + usuario_codigo
	if cfg.ProdutoNome == "" || cfg.InquilinoCodigo == "" || cfg.UsuarioCodigo == "" {
		return map[string]any{"ok": false, "motivo": "contexto_incompleto"}
	}

	chaveCooldown := chaveCooldown(cfg.ProdutoNome, cfg.InquilinoCodigo, cfg.UsuarioCodigo)
	untilMs := calcularCooldownAteMs(cfg.CooldownHours)

	// Enviamos exatamente o produto_nome informado.
	payload := map[string]any{
		"produto_nome":     cfg.ProdutoNome,
		"inquilino_codigo": cfg.InquilinoCodigo,
		"inquilino_nome":   cfg.InquilinoNome,
		"usuario_codigo":   cfg.UsuarioCodigo,
		"usuario_nome":     cfg.UsuarioNome,
		"usuario_telefone": cfg.UsuarioTelefone,
		"usuario_email":    normalizarEmail(cfg.UsuarioEmail),
	}

	return map[string]any{
		"ok":              true,
		"chave_cooldown":  chaveCooldown,
		"cooldown_ate_ms": untilMs,
		"payload":         payload,
	}
}

func decidir(this js.Value, args []js.Value) any {
	// Entrada:
	// - args[0] = cfg (para cooldownHours)
	// - args[1] = resposta JSON do servidor (obj)
	if len(args) < 2 {
		return map[string]any{"abrir": false, "motivo": "sem_dados"}
	}
	cfg := lerCfg(args[0])
	data := args[1]

	untilMs := calcularCooldownAteMs(cfg.CooldownHours)

	// Fail-closed.
	if data.IsUndefined() || data.IsNull() {
		return map[string]any{"abrir": false, "motivo": "resposta_invalida", "aplicar_cooldown": true, "cooldown_ate_ms": untilMs}
	}

	// Esperado: {pode_abrir:bool, id:string, produto:string}
	podeAbrir := data.Get("pode_abrir")
	if !podeAbrir.Truthy() {
		return map[string]any{"abrir": false, "motivo": "nao_pode_abrir", "aplicar_cooldown": true, "cooldown_ate_ms": untilMs}
	}
	id := strings.TrimSpace(data.Get("id").String())
	produtoRota := strings.TrimSpace(data.Get("produto").String())
	if id == "" || produtoRota == "" {
		// Não dá para montar URL segura.
		return map[string]any{"abrir": false, "motivo": "sem_produto_ou_id", "aplicar_cooldown": true, "cooldown_ate_ms": untilMs}
	}

	return map[string]any{
		"abrir":            true,
		"id":               id,
		"produto_rota":     produtoRota,
		"aplicar_cooldown": true,
		"cooldown_ate_ms":  untilMs,
	}
}

func lerCfg(v js.Value) cfgWidget {
	// Leitura defensiva: tudo é best-effort.
	getStr := func(k string) string {
		if v.Type() != js.TypeObject {
			return ""
		}
		vv := v.Get(k)
		if vv.IsUndefined() || vv.IsNull() {
			return ""
		}
		return strings.TrimSpace(vv.String())
	}
	getNum := func(k string) float64 {
		if v.Type() != js.TypeObject {
			return 0
		}
		vv := v.Get(k)
		if vv.IsUndefined() || vv.IsNull() {
			return 0
		}
		return vv.Float()
	}

	return cfgWidget{
		ProdutoNome:        getStr("produto_nome"),
		InquilinoCodigo:    getStr("inquilino_codigo"),
		InquilinoNome:      getStr("inquilino_nome"),
		UsuarioCodigo:      getStr("usuario_codigo"),
		UsuarioNome:        getStr("usuario_nome"),
		UsuarioTelefone:    getStr("usuario_telefone"),
		UsuarioEmail:       getStr("usuario_email"),
		CooldownHours:      getNum("cooldownHours"),
		DataMinimaAbertura: getStr("data_minima_abertura"),
	}
}

func normalizarEmail(email string) string {
	return strings.ToLower(strings.TrimSpace(email))
}

func chaveCooldown(produto, inquilino, usuarioCodigo string) string {
	// Prefixo de storage atualizado para o novo nome do projeto.
	return "eli-nps:cooldown:" + produto + ":" + inquilino + ":" + usuarioCodigo
}

func calcularCooldownAteMs(hours float64) int64 {
	if hours <= 0 {
		hours = 24
	}
	agora := js.Global().Get("Date").Call("now").Int()
	return int64(agora) + int64(hours*3600*1000)
}

func antesDaDataMinima(s string) bool {
	// Aceita somente ISO (data) YYYY-MM-DD.
	v := strings.TrimSpace(s)
	if v == "" {
		return false
	}
	m := dataISORe.FindStringSubmatch(v)
	if m == nil {
		return false
	}
	// new Date(ano, mes-1, dia, 0, 0, 0, 0)
	ano := m[1]
	mes := m[2]
	dia := m[3]

	// Converte para números via JS (simplifica validação e compatibilidade de timezone).
	nAno := js.Global().Get("Number").Invoke(ano).Int()
	nMes := js.Global().Get("Number").Invoke(mes).Int()
	nDia := js.Global().Get("Number").Invoke(dia).Int()
	if nAno <= 0 || nMes < 1 || nMes > 12 || nDia < 1 || nDia > 31 {
		return false
	}
	dataMin := js.Global().Get("Date").New(nAno, nMes-1, nDia, 0, 0, 0, 0)
	agora := js.Global().Get("Date").New()
	return agora.Call("getTime").Int() < dataMin.Call("getTime").Int()
}
