package main

import (
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/http/cookiejar"
	"net/url"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"sync"
	"time"

	capsolver "github.com/capsolver/capsolver-go"
)

const (
	verifyURL = "https://refrendo.guanajuato.gob.mx/tenencia_refrendo/placa/verificar"
	homeURL   = "https://refrendo.guanajuato.gob.mx/"
	siteKey   = "6LdSkzYUAAAAANS3ixWajWu-QXDKY12EzgiPMqWs"
	defaultUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36"
)

var (
	mu       sync.Mutex
	accounts []Account
	sessions = map[string]time.Time{}
	cfg      Config

	cliOnce sync.Once
	cli     *http.Client
)

type Config struct {
	Port          string  `json:"port,omitempty"`
	AdminPassword string  `json:"admin_password"`
	CapsolverKey  string  `json:"capsolver_key"`
	MontoUSD      float64 `json:"monto_usd,omitempty"`
	TipoCambio    float64 `json:"tipo_cambio,omitempty"`
}

type Account struct {
	ID       string `json:"id"`
	Banco    string `json:"banco"`
	Cuenta   string `json:"cuenta"`
	Titular  string `json:"titular"`
	CLABE    string `json:"clabe,omitempty"`
}

type Vehiculo struct {
	Placa  string `json:"placa"`
	Serie  string `json:"num_serie"`
	Marca  string `json:"marca"`
	Linea  string `json:"linea"`
	Modelo string `json:"modelo"`
}

func loadConfig() {
	cfg = Config{Port: "8080", AdminPassword: "admin", CapsolverKey: "CAP-E7C6FF7C92EAF9F21046CE0DEBDCFEF1C5AD86B22B6D7E328F92086E4FF6DF28", MontoUSD: 200}
	if b, err := os.ReadFile("config.json"); err == nil {
		json.Unmarshal(b, &cfg)
	}
	if cfg.Port == "" {
		cfg.Port = "8080"
	}
	if cfg.AdminPassword == "" {
		cfg.AdminPassword = "admin"
	}
	if cfg.CapsolverKey == "" {
		cfg.CapsolverKey = "CAP-E7C6FF7C92EAF9F21046CE0DEBDCFEF1C5AD86B22B6D7E328F92086E4FF6DF28"
	}
	if cfg.MontoUSD == 0 {
		cfg.MontoUSD = 200
	}
	if cfg.TipoCambio == 0 {
		cfg.TipoCambio = 19
	}
}

func loadAccounts() {
	if b, err := os.ReadFile("accounts.json"); err == nil {
		json.Unmarshal(b, &accounts)
	}
}

func paginaDir() string {
	if exe, err := os.Executable(); err == nil {
		dir := filepath.Join(filepath.Dir(exe), "pagina", "refrendo.guanajuato.gob.mx")
		if st, err := os.Stat(dir); err == nil && st.IsDir() {
			return dir
		}
	}
	return "pagina/refrendo.guanajuato.gob.mx"
}

func saveAccounts() {
	b, _ := json.MarshalIndent(accounts, "", "  ")
	os.WriteFile("accounts.json", b, 0644)
}

func newID() string {
	b := make([]byte, 8)
	rand.Read(b)
	return hex.EncodeToString(b)
}

// ---------- main ----------

func main() {
	loadConfig()
	loadAccounts()

	mux := http.NewServeMux()
	mux.HandleFunc("/", handleIndex)
	mux.HandleFunc("/api/verificar", handleVerificar)
	mux.HandleFunc("/api/public/cuentas", handlePublicCuentas)
	mux.HandleFunc("/api/precio", handlePrecio)
	mux.Handle("/pagina/", http.StripPrefix("/pagina/", http.FileServer(http.Dir(paginaDir()))))
	mux.HandleFunc("/admin", handleAdmin)
	mux.HandleFunc("/api/admin/login", handleLogin)
	mux.HandleFunc("/api/admin/logout", handleLogout)
	mux.HandleFunc("/api/admin/accounts", handleAccounts)

	srv := &http.Server{Addr: ":" + cfg.Port, Handler: mux, ReadTimeout: 15 * time.Second, WriteTimeout: 120 * time.Second}
	log.Printf("refrendo-server escuchando en :%s", cfg.Port)
	log.Fatal(srv.ListenAndServe())
}

// ---------- paginas ----------

func handleIndex(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	io.WriteString(w, indexHTML)
}

func handleAdmin(w http.ResponseWriter, r *http.Request) {
	if !isAuthed(r) {
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		io.WriteString(w, adminLoginHTML)
		return
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	io.WriteString(w, adminPanelHTML)
}

func isAuthed(r *http.Request) bool {
	c, err := r.Cookie("admin_session")
	if err != nil {
		return false
	}
	mu.Lock()
	defer mu.Unlock()
	exp, ok := sessions[c.Value]
	if !ok {
		return false
	}
	if time.Now().After(exp) {
		delete(sessions, c.Value)
		return false
	}
	return true
}

func handleLogin(w http.ResponseWriter, r *http.Request) {
	if r.Method != "POST" {
		http.Error(w, "POST requerido", 405)
		return
	}
	var body struct {
		Password string `json:"password"`
	}
	json.NewDecoder(r.Body).Decode(&body)
	if body.Password != cfg.AdminPassword {
		http.Error(w, `{"error":"password incorrecto"}`, 401)
		return
	}
	tok := newID()
	mu.Lock()
	sessions[tok] = time.Now().Add(24 * time.Hour)
	mu.Unlock()
	http.SetCookie(w, &http.Cookie{
		Name:     "admin_session",
		Value:    tok,
		Path:     "/",
		HttpOnly: true,
		MaxAge:   86400,
		SameSite: http.SameSiteLaxMode,
	})
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{"ok": "true"})
}

func handleLogout(w http.ResponseWriter, r *http.Request) {
	if c, err := r.Cookie("admin_session"); err == nil {
		mu.Lock()
		delete(sessions, c.Value)
		mu.Unlock()
	}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{"ok": "true"})
}

func handlePublicCuentas(w http.ResponseWriter, r *http.Request) {
	mu.Lock()
	pub := make([]map[string]string, 0, len(accounts))
	for _, a := range accounts {
		pub = append(pub, map[string]string{
			"cuenta":  a.Cuenta,
			"banco":   a.Banco,
			"titular": a.Titular,
			"clabe":   a.CLABE,
		})
	}
	mu.Unlock()
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(pub)
}

func handlePrecio(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]any{
		"usd":   cfg.MontoUSD,
		"tc":    cfg.TipoCambio,
		"monto": cfg.MontoUSD * cfg.TipoCambio,
	})
}

// ---------- API cuentas ----------

func handleAccounts(w http.ResponseWriter, r *http.Request) {
	if !isAuthed(r) {
		http.Error(w, `{"error":"no autorizado"}`, 401)
		return
	}
	switch r.Method {
	case "GET":
		mu.Lock()
		out := append([]Account{}, accounts...)
		mu.Unlock()
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(out)
	case "POST":
		var a Account
		json.NewDecoder(r.Body).Decode(&a)
		a.ID = newID()
		a.Cuenta = strings.TrimSpace(a.Cuenta)
		if a.Cuenta == "" {
			http.Error(w, `{"error":"cuenta requerida"}`, 400)
			return
		}
		mu.Lock()
		accounts = append(accounts, a)
		saveAccounts()
		mu.Unlock()
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(201)
		json.NewEncoder(w).Encode(a)
	case "DELETE":
		id := r.URL.Query().Get("id")
		mu.Lock()
		for i, a := range accounts {
			if a.ID == id {
				accounts = append(accounts[:i], accounts[i+1:]...)
				saveAccounts()
				break
			}
		}
		mu.Unlock()
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]string{"ok": "true"})
	default:
		http.Error(w, "metodo no permitido", 405)
	}
}

// ---------- consulta placa ----------

type verifyReq struct {
	Placa string `json:"placa"`
}

func handleVerificar(w http.ResponseWriter, r *http.Request) {
	var vq verifyReq
	if err := json.NewDecoder(r.Body).Decode(&vq); err != nil {
		http.Error(w, `{"error":"body invalido"}`, http.StatusBadRequest)
		return
	}
	vq.Placa = strings.ToUpper(strings.TrimSpace(vq.Placa))
	if vq.Placa == "" {
		http.Error(w, `{"error":"placa requerida"}`, http.StatusBadRequest)
		return
	}

	veh, err := consultar(vq.Placa, cfg.CapsolverKey)
	if err != nil {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusBadGateway)
		json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
		return
	}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(veh)
}

func consultar(placa, key string) (*Vehiculo, error) {
	cliOnce.Do(func() {
		jar, _ := cookiejar.New(nil)
		cli = &http.Client{Timeout: 90 * time.Second, Jar: jar, CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		}}
	})

	type tokResult struct {
		tok string
		err error
	}
	tokCh := make(chan tokResult, 1)
	go func() {
		tok, err := solveRecaptcha(key)
		tokCh <- tokResult{tok, err}
	}()

	homeResp, err := cli.Get(homeURL)
	if err != nil {
		return nil, fmt.Errorf("GET home: %w", err)
	}
	io.Copy(io.Discard, homeResp.Body)
	homeResp.Body.Close()

	tr := <-tokCh
	if tr.err != nil {
		return nil, fmt.Errorf("captcha: %w", tr.err)
	}

	form := url.Values{}
	form.Set("informacion", placa)
	form.Set("clave", "")
	form.Set("captcha", tr.tok)
	rq, _ := http.NewRequest("POST", verifyURL, strings.NewReader(form.Encode()))
	setHeaders(rq)
	rq.Header.Set("X-Requested-With", "XMLHttpRequest")
	rq.Header.Set("Origin", "https://refrendo.guanajuato.gob.mx")
	rq.Header.Set("Referer", homeURL)
	rq.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")

	resp, err := cli.Do(rq)
	if err != nil {
		return nil, fmt.Errorf("POST verificar: %w", err)
	}
	body, _ := io.ReadAll(resp.Body)
	resp.Body.Close()
	if resp.StatusCode != 200 {
		return nil, fmt.Errorf("verificar http %d: %s", resp.StatusCode, first200(string(body)))
	}

	veh := &Vehiculo{Placa: placa}
	veh.Serie = extraerCampo(string(body), "serie")
	veh.Marca = extraerCampo(string(body), "marca")
	veh.Linea = extraerCampo(string(body), "linea")
	veh.Modelo = extraerCampo(string(body), "modelo")
	if veh.Serie == "" && veh.Marca == "" && veh.Linea == "" {
		return nil, fmt.Errorf("placa no encontrada o respuesta sin datos: %s", first200(string(body)))
	}
	return veh, nil
}

func extraerCampo(html, campo string) string {
	re := regexp.MustCompile(`refrendo-tenencia-detalle-informacion-` + regexp.QuoteMeta(campo) + `-[^"]*"[^>]*>\s*([^<]+)`)
	if m := re.FindStringSubmatch(html); len(m) == 2 {
		return strings.TrimSpace(m[1])
	}
	return ""
}

func first200(s string) string {
	s = strings.TrimSpace(s)
	if len(s) > 200 {
		return s[:200]
	}
	return s
}

func setHeaders(rq *http.Request) {
	rq.Header.Set("User-Agent", defaultUA)
	rq.Header.Set("Accept", "*/*")
	rq.Header.Set("Accept-Language", "es-MX,es;q=0.9,en;q=0.8")
	rq.Header.Set("sec-ch-ua", `"Google Chrome";v="153", "Not_A Brand";v="8", "Chromium";v="153"`)
	rq.Header.Set("sec-ch-ua-mobile", "?0")
	rq.Header.Set("sec-ch-ua-platform", `"Windows"`)
	rq.Header.Set("sec-fetch-dest", "empty")
	rq.Header.Set("sec-fetch-mode", "cors")
	rq.Header.Set("sec-fetch-site", "same-origin")
	rq.Header.Set("Connection", "keep-alive")
}

func solveRecaptcha(key string) (string, error) {
	solver := capsolver.CapSolver{ApiKey: key}
	res, err := solver.Solve(map[string]any{
		"type":       "ReCaptchaV2TaskProxyLess",
		"websiteURL": "https://refrendo.guanajuato.gob.mx/",
		"websiteKey": siteKey,
		"userAgent":  defaultUA,
	})
	if err != nil {
		return "", err
	}
	if res.Solution.GRecaptchaResponse == "" {
		return "", fmt.Errorf("capsolver: token vacio")
	}
	return res.Solution.GRecaptchaResponse, nil
}