package infra

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"os/exec"
	"runtime"
	"strings"
	"time"
)

// MasOidcLoopbackConfig configura el flujo OAuth2 PKCE con loopback HTTP
// contra Matrix Authentication Service (MAS).
type MasOidcLoopbackConfig struct {
	// Issuer es la URL base del MAS. Debe terminar en "/".
	// La funcion hace GET a {Issuer}.well-known/openid-configuration para descubrir endpoints.
	Issuer string

	// ClientID es el ULID del client registrado en MAS.
	// El client debe tener client_auth_method: none (public client PKCE).
	ClientID string

	// Scopes a solicitar. Si vacio usa ["openid", "urn:matrix:org.matrix.msc2967.client:api:*"].
	Scopes []string

	// LoopbackPort es el puerto local donde escucha el callback.
	// Debe coincidir con el redirect_uri registrado en MAS (http://127.0.0.1:{port}/callback).
	// Si 0, elige un puerto libre dinamicamente.
	LoopbackPort int

	// OpenBrowser abre el browser del SO automaticamente si es true.
	// Si false, imprime la URL a stdout y espera que el caller la abra.
	OpenBrowser bool

	// TimeoutSeconds es el tiempo maximo esperando el callback. Default 300.
	TimeoutSeconds int
}

// MasOidcLoopbackResult contiene los tokens devueltos por MAS tras el intercambio.
type MasOidcLoopbackResult struct {
	// AccessToken es el Bearer token para usar contra Synapse.
	AccessToken string `json:"access_token"`

	// RefreshToken permite renovar el access token sin re-autenticar.
	RefreshToken string `json:"refresh_token"`

	// ExpiresIn es el tiempo de vida del access token en segundos.
	ExpiresIn int `json:"expires_in"`

	// TokenType es el tipo de token, normalmente "Bearer".
	TokenType string `json:"token_type"`

	// Scope es la lista de scopes concedidos (space-separated).
	Scope string `json:"scope"`

	// IDToken es el JWT de identidad OIDC (puede estar vacio si no se pidio openid).
	IDToken string `json:"id_token,omitempty"`
}

// oidcDiscovery es la respuesta de .well-known/openid-configuration.
type oidcDiscovery struct {
	AuthorizationEndpoint string `json:"authorization_endpoint"`
	TokenEndpoint         string `json:"token_endpoint"`
}

// MasOidcLoopback ejecuta el flujo OAuth2 Authorization Code + PKCE contra MAS
// usando un servidor HTTP loopback para recibir el callback.
//
// Flujo:
//  1. Discovery de endpoints via .well-known/openid-configuration.
//  2. Generacion de code_verifier/challenge PKCE y state anti-CSRF.
//  3. Arranque de servidor loopback en 127.0.0.1:{LoopbackPort}.
//  4. Apertura del browser (o impresion de URL si OpenBrowser=false).
//  5. Espera del callback con el authorization code.
//  6. Intercambio del code por tokens via POST al token_endpoint.
//  7. Devolucion de MasOidcLoopbackResult.
func MasOidcLoopback(cfg MasOidcLoopbackConfig) (*MasOidcLoopbackResult, error) {
	// 1. Validar inputs
	if cfg.Issuer == "" {
		return nil, fmt.Errorf("mas_oidc_loopback: Issuer no puede estar vacio")
	}
	if !strings.HasSuffix(cfg.Issuer, "/") {
		return nil, fmt.Errorf("mas_oidc_loopback: Issuer debe terminar en '/' (got %q)", cfg.Issuer)
	}
	if cfg.ClientID == "" {
		return nil, fmt.Errorf("mas_oidc_loopback: ClientID no puede estar vacio")
	}
	if cfg.LoopbackPort < 0 {
		return nil, fmt.Errorf("mas_oidc_loopback: LoopbackPort debe ser >= 0")
	}

	timeout := time.Duration(cfg.TimeoutSeconds) * time.Second
	if cfg.TimeoutSeconds <= 0 {
		timeout = 300 * time.Second
	}

	scopes := cfg.Scopes
	if len(scopes) == 0 {
		scopes = []string{"openid", "urn:matrix:org.matrix.msc2967.client:api:*"}
	}

	// 2. Discovery OIDC
	discovery, err := masOidcDiscover(cfg.Issuer)
	if err != nil {
		return nil, fmt.Errorf("mas_oidc_loopback: discovery failed: %w", err)
	}

	// 3. PKCE: code_verifier + code_challenge
	verifier, challenge, err := masOidcPKCE()
	if err != nil {
		return nil, fmt.Errorf("mas_oidc_loopback: pkce generation failed: %w", err)
	}

	// 4. State anti-CSRF
	state, err := masOidcRandomBase64URL(32)
	if err != nil {
		return nil, fmt.Errorf("mas_oidc_loopback: state generation failed: %w", err)
	}

	// 5. Arrancar loopback server
	listener, port, err := masOidcStartListener(cfg.LoopbackPort)
	if err != nil {
		return nil, fmt.Errorf("mas_oidc_loopback: no se pudo abrir puerto loopback: %w", err)
	}

	redirectURI := fmt.Sprintf("http://127.0.0.1:%d/callback", port)

	// Canal para recibir el code o error desde el handler HTTP
	codeCh := make(chan string, 1)
	errCh := make(chan error, 1)

	mux := http.NewServeMux()
	mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
		q := r.URL.Query()

		// Validar state anti-CSRF
		if q.Get("state") != state {
			errCh <- fmt.Errorf("mas_oidc_loopback: state mismatch (posible CSRF) — esperado %q, recibido %q", state, q.Get("state"))
			w.Header().Set("Content-Type", "text/html; charset=utf-8")
			w.WriteHeader(http.StatusBadRequest)
			_, _ = w.Write([]byte("<html><body><h2>Error: state mismatch. Por favor cierra esta ventana.</h2></body></html>"))
			return
		}

		// Verificar error del proveedor
		if errParam := q.Get("error"); errParam != "" {
			desc := q.Get("error_description")
			errCh <- fmt.Errorf("mas_oidc_loopback: proveedor devolvio error %q: %s", errParam, desc)
			w.Header().Set("Content-Type", "text/html; charset=utf-8")
			w.WriteHeader(http.StatusBadRequest)
			_, _ = w.Write([]byte(fmt.Sprintf("<html><body><h2>Error de autorizacion: %s</h2></body></html>", desc)))
			return
		}

		code := q.Get("code")
		if code == "" {
			errCh <- fmt.Errorf("mas_oidc_loopback: callback sin 'code'")
			w.Header().Set("Content-Type", "text/html; charset=utf-8")
			w.WriteHeader(http.StatusBadRequest)
			_, _ = w.Write([]byte("<html><body><h2>Error: no se recibio authorization code.</h2></body></html>"))
			return
		}

		// Responder al browser con mensaje de exito
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(`<!DOCTYPE html>
<html lang="es">
<head><meta charset="utf-8"><title>Login completo</title></head>
<body style="font-family:sans-serif;text-align:center;padding:3em;">
<h2>Login completo</h2>
<p>Puedes cerrar esta ventana y volver a la aplicacion.</p>
</body>
</html>`))

		codeCh <- code
	})

	srv := &http.Server{Handler: mux}

	// Arrancar el servidor en goroutine
	srvErrCh := make(chan error, 1)
	go func() {
		if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
			srvErrCh <- err
		}
	}()

	// 6. Construir URL de autorización
	authURL := masOidcBuildAuthURL(
		discovery.AuthorizationEndpoint,
		cfg.ClientID,
		redirectURI,
		strings.Join(scopes, " "),
		state,
		challenge,
	)

	// 7. Abrir browser o imprimir URL
	if cfg.OpenBrowser {
		if err := masOidcOpenBrowser(authURL); err != nil {
			// No es fatal: continuamos y el usuario puede abrir manualmente
			fmt.Printf("mas_oidc_loopback: no se pudo abrir el browser automaticamente.\nAbre esta URL manualmente:\n%s\n", authURL)
		}
	} else {
		fmt.Printf("Abre esta URL en tu browser para autenticarte:\n%s\n", authURL)
	}

	// 8. Esperar callback con timeout
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()

	var code string
	select {
	case code = <-codeCh:
		// ok
	case callbackErr := <-errCh:
		_ = srv.Shutdown(context.Background())
		return nil, callbackErr
	case <-ctx.Done():
		_ = srv.Shutdown(context.Background())
		return nil, fmt.Errorf("mas_oidc_loopback: timeout esperando callback despues de %v", timeout)
	case srvErr := <-srvErrCh:
		return nil, fmt.Errorf("mas_oidc_loopback: servidor loopback fallo: %w", srvErr)
	}

	// 9. Shutdown graceful del servidor loopback
	shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer shutdownCancel()
	_ = srv.Shutdown(shutdownCtx)

	// 10. Intercambiar code por tokens
	result, err := masOidcExchangeCode(
		discovery.TokenEndpoint,
		cfg.ClientID,
		code,
		redirectURI,
		verifier,
	)
	if err != nil {
		return nil, fmt.Errorf("mas_oidc_loopback: token exchange failed: %w", err)
	}

	return result, nil
}

// masOidcHTTPClient es el cliente HTTP usado por masOidcDiscover y masOidcExchangeCode.
// Tiene timeout de 15s. Puede ser reemplazado en tests.
var masOidcHTTPClient = &http.Client{Timeout: 15 * time.Second}

// masOidcDiscover obtiene los endpoints OIDC desde .well-known/openid-configuration.
func masOidcDiscover(issuer string) (*oidcDiscovery, error) {
	discoveryURL := issuer + ".well-known/openid-configuration"
	resp, err := masOidcHTTPClient.Get(discoveryURL) //nolint:gosec
	if err != nil {
		return nil, fmt.Errorf("GET %s: %w", discoveryURL, err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("discovery HTTP %d: %s", resp.StatusCode, string(body))
	}

	var d oidcDiscovery
	if err := json.NewDecoder(resp.Body).Decode(&d); err != nil {
		return nil, fmt.Errorf("parsing discovery JSON: %w", err)
	}
	if d.AuthorizationEndpoint == "" {
		return nil, fmt.Errorf("discovery: authorization_endpoint vacio")
	}
	if d.TokenEndpoint == "" {
		return nil, fmt.Errorf("discovery: token_endpoint vacio")
	}
	return &d, nil
}

// masOidcPKCE genera un code_verifier aleatorio y su code_challenge SHA256/base64url.
func masOidcPKCE() (verifier, challenge string, err error) {
	verifier, err = masOidcRandomBase64URL(32) // 32 bytes -> 43 chars base64url
	if err != nil {
		return "", "", err
	}
	h := sha256.Sum256([]byte(verifier))
	challenge = base64.RawURLEncoding.EncodeToString(h[:])
	return verifier, challenge, nil
}

// masOidcRandomBase64URL genera n bytes aleatorios codificados en base64url sin padding.
func masOidcRandomBase64URL(n int) (string, error) {
	b := make([]byte, n)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	return base64.RawURLEncoding.EncodeToString(b), nil
}

// masOidcStartListener abre un listener TCP en 127.0.0.1:{port}.
// Si port=0, elige un puerto libre y devuelve el puerto asignado.
func masOidcStartListener(port int) (net.Listener, int, error) {
	addr := fmt.Sprintf("127.0.0.1:%d", port)
	l, err := net.Listen("tcp", addr)
	if err != nil {
		return nil, 0, err
	}
	assignedPort := l.Addr().(*net.TCPAddr).Port
	return l, assignedPort, nil
}

// masOidcBuildAuthURL construye la URL de autorización OAuth2 con PKCE.
func masOidcBuildAuthURL(authEndpoint, clientID, redirectURI, scope, state, challenge string) string {
	u, _ := url.Parse(authEndpoint)
	q := u.Query()
	q.Set("response_type", "code")
	q.Set("client_id", clientID)
	q.Set("redirect_uri", redirectURI)
	q.Set("scope", scope)
	q.Set("state", state)
	q.Set("code_challenge", challenge)
	q.Set("code_challenge_method", "S256")
	u.RawQuery = q.Encode()
	return u.String()
}

// masOidcOpenBrowser abre la URL en el browser predeterminado del SO.
func masOidcOpenBrowser(rawURL string) error {
	var cmd *exec.Cmd
	switch runtime.GOOS {
	case "linux":
		cmd = exec.Command("xdg-open", rawURL)
	case "darwin":
		cmd = exec.Command("open", rawURL)
	case "windows":
		cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", rawURL)
	default:
		return fmt.Errorf("plataforma no soportada para abrir browser: %s", runtime.GOOS)
	}
	return cmd.Start()
}

// masOidcExchangeCode intercambia el authorization code por tokens via POST al token_endpoint.
func masOidcExchangeCode(tokenEndpoint, clientID, code, redirectURI, verifier string) (*MasOidcLoopbackResult, error) {
	formData := url.Values{}
	formData.Set("grant_type", "authorization_code")
	formData.Set("code", code)
	formData.Set("redirect_uri", redirectURI)
	formData.Set("client_id", clientID)
	formData.Set("code_verifier", verifier)

	resp, err := masOidcHTTPClient.PostForm(tokenEndpoint, formData) //nolint:gosec
	if err != nil {
		return nil, fmt.Errorf("POST %s: %w", tokenEndpoint, err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("leyendo respuesta del token endpoint: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("token endpoint HTTP %d: %s", resp.StatusCode, string(body))
	}

	var result MasOidcLoopbackResult
	if err := json.Unmarshal(body, &result); err != nil {
		return nil, fmt.Errorf("parsing token response JSON: %w", err)
	}
	if result.AccessToken == "" {
		return nil, fmt.Errorf("token response sin access_token: %s", string(body))
	}

	return &result, nil
}
