package services

import (
	"context"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/sujit-baniya/fiber-boilerplate/app"
	"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"golang.org/x/oauth2/microsoft"
)

const (
	UmindOAuthGoogle    = "google"
	UmindOAuthMicrosoft = "microsoft"
)

// CorreoResumen es el formato común en el que enviar_correo/leer_bandeja
// devuelven un mensaje al agente, sin importar el proveedor real detrás.
type CorreoResumen struct {
	De       string `json:"de"`
	Asunto   string `json:"asunto"`
	Fecha    string `json:"fecha"`
	Extracto string `json:"extracto"`
}

var umindOAuthHTTPClient = &http.Client{Timeout: 15 * time.Second}

// firmarState / verificarState arman el parámetro state del flujo OAuth
// autoverificable (agenteID + nonce + HMAC con APP_KEY) — evita necesitar una
// tabla de "estados pendientes": si la firma es válida, el state no fue
// alterado desde que lo generamos nosotros.
func firmarState(agenteID uint) (string, error) {
	nonce := make([]byte, 8)
	if _, err := rand.Read(nonce); err != nil {
		return "", err
	}
	payload := fmt.Sprintf("%d.%s", agenteID, hex.EncodeToString(nonce))
	mac := hmac.New(sha256.New, []byte(app.Http.Server.Key))
	mac.Write([]byte(payload))
	return payload + "." + hex.EncodeToString(mac.Sum(nil)), nil
}

func verificarState(state string) (uint, error) {
	partes := strings.Split(state, ".")
	if len(partes) != 3 {
		return 0, fmt.Errorf("formato de state inválido")
	}
	payload := partes[0] + "." + partes[1]
	mac := hmac.New(sha256.New, []byte(app.Http.Server.Key))
	mac.Write([]byte(payload))
	esperada := hex.EncodeToString(mac.Sum(nil))
	if !hmac.Equal([]byte(esperada), []byte(partes[2])) {
		return 0, fmt.Errorf("firma de state inválida")
	}
	agenteID, err := strconv.ParseUint(partes[0], 10, 64)
	if err != nil {
		return 0, fmt.Errorf("agente_id inválido en state: %w", err)
	}
	return uint(agenteID), nil
}

func redirectURLOAuth(proveedor string) string {
	return strings.TrimRight(app.Http.Server.Url, "/") + "/app/umind/conexiones/callback/" + proveedor
}

func oauth2ConfigPara(proveedor string) (*oauth2.Config, error) {
	switch proveedor {
	case UmindOAuthGoogle:
		if app.Http.OAuth.GoogleClientID == "" || app.Http.OAuth.GoogleClientSecret == "" {
			return nil, fmt.Errorf("Google OAuth no está configurado en el servidor (GOOGLE_OAUTH_CLIENT_ID/GOOGLE_OAUTH_CLIENT_SECRET)")
		}
		return &oauth2.Config{
			ClientID:     app.Http.OAuth.GoogleClientID,
			ClientSecret: app.Http.OAuth.GoogleClientSecret,
			RedirectURL:  redirectURLOAuth(proveedor),
			Scopes: []string{
				"https://www.googleapis.com/auth/gmail.send",
				"https://www.googleapis.com/auth/gmail.readonly",
				"https://www.googleapis.com/auth/userinfo.email",
			},
			Endpoint: google.Endpoint,
		}, nil
	case UmindOAuthMicrosoft:
		if app.Http.OAuth.MSClientID == "" || app.Http.OAuth.MSClientSecret == "" {
			return nil, fmt.Errorf("Microsoft OAuth no está configurado en el servidor (MS_OAUTH_CLIENT_ID/MS_OAUTH_CLIENT_SECRET)")
		}
		return &oauth2.Config{
			ClientID:     app.Http.OAuth.MSClientID,
			ClientSecret: app.Http.OAuth.MSClientSecret,
			RedirectURL:  redirectURLOAuth(proveedor),
			Scopes: []string{
				"offline_access", "openid", "email",
				"https://graph.microsoft.com/Mail.Send",
				"https://graph.microsoft.com/Mail.Read",
			},
			Endpoint: microsoft.AzureADEndpoint("common"),
		}, nil
	default:
		return nil, fmt.Errorf("proveedor desconocido: %s", proveedor)
	}
}

// IniciarConexionOAuth arma la URL de autorización a la que hay que
// redirigir al staff. prompt=consent en Google fuerza a que siempre vuelva
// un refresh_token (si no, Google solo lo manda la primera vez que el
// usuario autoriza la app, nunca más).
func IniciarConexionOAuth(proveedor string, agenteID uint) (string, error) {
	cfg, err := oauth2ConfigPara(proveedor)
	if err != nil {
		return "", err
	}
	state, err := firmarState(agenteID)
	if err != nil {
		return "", err
	}
	opts := []oauth2.AuthCodeOption{oauth2.AccessTypeOffline}
	if proveedor == UmindOAuthGoogle {
		opts = append(opts, oauth2.SetAuthURLParam("prompt", "consent"))
	}
	return cfg.AuthCodeURL(state, opts...), nil
}

// CompletarConexionOAuth intercambia el code por tokens, identifica la
// cuenta conectada y guarda la conexión cifrada. Reemplaza cualquier
// conexión previa activa del agente (una cuenta de correo a la vez).
func CompletarConexionOAuth(proveedor, code, state string) (*models.UmindConexion, error) {
	agenteID, err := verificarState(state)
	if err != nil {
		return nil, fmt.Errorf("state inválido: %w", err)
	}
	cfg, err := oauth2ConfigPara(proveedor)
	if err != nil {
		return nil, err
	}
	tok, err := cfg.Exchange(context.Background(), code)
	if err != nil {
		return nil, fmt.Errorf("no se pudo intercambiar el código de autorización: %w", err)
	}
	if tok.RefreshToken == "" {
		return nil, fmt.Errorf("el proveedor no devolvió un refresh_token — revocá el acceso de esta app en tu cuenta y volvé a conectar")
	}

	email, err := obtenerEmailDeCuenta(proveedor, tok.AccessToken)
	if err != nil {
		log.Printf("[UMIND_OAUTH] no se pudo obtener el email de la cuenta conectada (%s): %v", proveedor, err)
	}

	accessEnc, err := CifrarSecretoUmind(tok.AccessToken)
	if err != nil {
		return nil, err
	}
	refreshEnc, err := CifrarSecretoUmind(tok.RefreshToken)
	if err != nil {
		return nil, err
	}

	if err := models.DesactivarConexionesDelAgente(agenteID); err != nil {
		log.Printf("[UMIND_OAUTH] no se pudieron desactivar conexiones previas del agente %d: %v", agenteID, err)
	}
	conexion := &models.UmindConexion{
		AgenteID:        agenteID,
		Proveedor:       proveedor,
		Email:           email,
		AccessTokenEnc:  accessEnc,
		RefreshTokenEnc: refreshEnc,
		ExpiraEn:        tok.Expiry,
		Scopes:          strings.Join(cfg.Scopes, " "),
		Activo:          true,
	}
	if err := models.CreateUmindConexion(conexion); err != nil {
		return nil, err
	}
	return conexion, nil
}

func obtenerEmailDeCuenta(proveedor, accessToken string) (string, error) {
	var url string
	switch proveedor {
	case UmindOAuthGoogle:
		url = "https://www.googleapis.com/oauth2/v2/userinfo"
	case UmindOAuthMicrosoft:
		url = "https://graph.microsoft.com/v1.0/me"
	default:
		return "", fmt.Errorf("proveedor desconocido: %s", proveedor)
	}
	req, err := http.NewRequest(http.MethodGet, url, nil)
	if err != nil {
		return "", err
	}
	req.Header.Set("Authorization", "Bearer "+accessToken)
	resp, err := umindOAuthHTTPClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	var out struct {
		Email             string `json:"email"`             // Google
		Mail              string `json:"mail"`              // Microsoft
		UserPrincipalName string `json:"userPrincipalName"` // Microsoft, fallback si "mail" viene vacío
	}
	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
		return "", err
	}
	if out.Email != "" {
		return out.Email, nil
	}
	if out.Mail != "" {
		return out.Mail, nil
	}
	return out.UserPrincipalName, nil
}

// RefrescarSiVence renueva el access token si está vencido o a menos de 2
// minutos de vencer, y persiste el nuevo valor cifrado. Se llama justo antes
// de usar la conexión (enviar/leer correo), no por un cron aparte — ver nota
// de alcance en el plan: si en la práctica hace falta refresco proactivo, se
// agrega por pkg/services/cron_service.go sin tocar esta función.
func RefrescarSiVence(conexion *models.UmindConexion) error {
	if time.Now().Add(2 * time.Minute).Before(conexion.ExpiraEn) {
		return nil
	}
	cfg, err := oauth2ConfigPara(conexion.Proveedor)
	if err != nil {
		return err
	}
	refreshToken, err := DescifrarSecretoUmind(conexion.RefreshTokenEnc)
	if err != nil {
		return fmt.Errorf("no se pudo descifrar el refresh_token: %w", err)
	}

	nuevo, err := cfg.TokenSource(context.Background(), &oauth2.Token{RefreshToken: refreshToken}).Token()
	if err != nil {
		return fmt.Errorf("no se pudo refrescar el token: %w", err)
	}

	accessEnc, err := CifrarSecretoUmind(nuevo.AccessToken)
	if err != nil {
		return err
	}
	updates := map[string]interface{}{"access_token_enc": accessEnc, "expira_en": nuevo.Expiry}
	if nuevo.RefreshToken != "" && nuevo.RefreshToken != refreshToken {
		if refreshEnc, err := CifrarSecretoUmind(nuevo.RefreshToken); err == nil {
			updates["refresh_token_enc"] = refreshEnc
			conexion.RefreshTokenEnc = refreshEnc
		}
	}
	if err := models.UpdateUmindConexion(conexion.ID, updates); err != nil {
		log.Printf("[UMIND_OAUTH] no se pudo persistir el refresh del token (conexión %d): %v", conexion.ID, err)
	}
	conexion.AccessTokenEnc = accessEnc
	conexion.ExpiraEn = nuevo.Expiry
	return nil
}
