package userauth

import (
	"context"
	"crypto/subtle"
	"errors"
	"fmt"
	"html"
	"net"
	"net/http"
	"strings"
	"time"
)

const (
	oidcFlowLifetime     = 5 * time.Minute
	maxOIDCFlows         = 8
	oidcDesktopReturnURL = "agentteam://signin-complete"
)

type PingCAPAuthStart struct {
	FlowID           string `json:"flowId"`
	AuthorizationURL string `json:"authorizationUrl"`
}

type PingCAPAuthStatus struct {
	Status string      `json:"status"`
	Result *AuthResult `json:"result,omitempty"`
	Error  string      `json:"error,omitempty"`
}

type oidcFlow struct {
	ctx                      context.Context
	cancel                   context.CancelFunc
	serverURL                string
	redirectURI              string
	state                    string
	nonce                    string
	verifier                 string
	desktopOnboardingEnabled bool
	registrationCutoff       string
	locale                   string
	client                   *http.Client
	listener                 net.Listener
	expiresAt                time.Time
	processing               bool
	result                   *AuthResult
	err                      string
}

type oidcAuthorizationResponse struct {
	AuthorizationURL string `json:"authorization_url"`
	State            string `json:"state"`
	Nonce            string `json:"nonce"`
	CodeVerifier     string `json:"code_verifier"`
}

// BeginPingCAPAuth starts a bounded, one-shot loopback OIDC callback flow.
// Browser-facing callers receive only the authorization URL and an opaque flow
// identifier; PKCE material remains inside the daemon process.
func (m *Manager) BeginPingCAPAuth(ctx context.Context, rawServerURL string, desktopOnboardingEnabled bool, registrationCutoff string, preferredLocale ...string) (PingCAPAuthStart, error) {
	if err := m.ready(); err != nil {
		return PingCAPAuthStart{}, err
	}
	probe, err := m.Probe(ctx, rawServerURL)
	if err != nil {
		return PingCAPAuthStart{}, err
	}
	if !probe.PingCAPAuthEnabled {
		return PingCAPAuthStart{}, fmt.Errorf("PingCAP authentication is not enabled on this AgentTeam Server")
	}
	client, err := newHTTPClient(probe.ServerURL)
	if err != nil {
		return PingCAPAuthStart{}, err
	}
	listener, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		return PingCAPAuthStart{}, fmt.Errorf("start Desktop login callback: %w", err)
	}
	redirectURI := "http://" + listener.Addr().String() + "/oidc/callback"
	var authorization oidcAuthorizationResponse
	if err := doJSON(ctx, client, http.MethodPost, probe.ServerURL+"/api/v1/auth/oidc/authorize-url", map[string]string{
		"redirect_uri": redirectURI,
	}, "", &authorization); err != nil {
		_ = listener.Close()
		return PingCAPAuthStart{}, fmt.Errorf("start PingCAP authentication: %w", err)
	}
	if strings.TrimSpace(authorization.AuthorizationURL) == "" ||
		strings.TrimSpace(authorization.State) == "" ||
		strings.TrimSpace(authorization.Nonce) == "" ||
		strings.TrimSpace(authorization.CodeVerifier) == "" {
		_ = listener.Close()
		return PingCAPAuthStart{}, fmt.Errorf("AgentTeam Server returned an invalid PingCAP authentication flow")
	}
	flowID, err := newAuthID()
	if err != nil {
		_ = listener.Close()
		return PingCAPAuthStart{}, err
	}
	locale := "en"
	if len(preferredLocale) > 0 {
		locale = normalizeOIDCCompletionLocale(preferredLocale[0])
	}
	flowContext, cancelFlow := context.WithCancel(context.Background())
	flow := &oidcFlow{
		ctx:                      flowContext,
		cancel:                   cancelFlow,
		serverURL:                probe.ServerURL,
		redirectURI:              redirectURI,
		state:                    authorization.State,
		nonce:                    authorization.Nonce,
		verifier:                 authorization.CodeVerifier,
		desktopOnboardingEnabled: desktopOnboardingEnabled,
		registrationCutoff:       strings.TrimSpace(registrationCutoff),
		locale:                   locale,
		client:                   client,
		listener:                 listener,
		expiresAt:                time.Now().Add(oidcFlowLifetime),
	}

	m.oidcMu.Lock()
	m.pruneOIDCFlowsLocked(time.Now())
	if len(m.oidcFlows) >= maxOIDCFlows {
		m.oidcMu.Unlock()
		cancelFlow()
		_ = listener.Close()
		return PingCAPAuthStart{}, fmt.Errorf("too many Desktop login attempts are already active")
	}
	m.oidcFlows[flowID] = flow
	m.oidcMu.Unlock()

	mux := http.NewServeMux()
	mux.HandleFunc("/oidc/callback", m.handleOIDCCallback(flowID))
	server := &http.Server{
		Handler:           mux,
		ReadHeaderTimeout: 5 * time.Second,
	}
	go func() {
		if serveErr := server.Serve(listener); serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) && !errors.Is(serveErr, net.ErrClosed) {
			m.finishOIDCFlow(flowID, nil, "Desktop login callback stopped unexpectedly.")
		}
	}()
	time.AfterFunc(oidcFlowLifetime, func() { m.expireOIDCFlow(flowID) })

	return PingCAPAuthStart{
		FlowID:           flowID,
		AuthorizationURL: authorization.AuthorizationURL,
	}, nil
}

// PingCAPAuthFlowStatus returns a sanitized, consumable flow result. Completed
// and failed flows are removed after the first status read.
func (m *Manager) PingCAPAuthFlowStatus(flowID string) (PingCAPAuthStatus, error) {
	flowID = strings.TrimSpace(flowID)
	m.oidcMu.Lock()
	defer m.oidcMu.Unlock()
	m.pruneOIDCFlowsLocked(time.Now())
	flow, ok := m.oidcFlows[flowID]
	if !ok {
		return PingCAPAuthStatus{}, fmt.Errorf("Desktop login attempt was not found or has expired")
	}
	if flow.result != nil {
		result := *flow.result
		delete(m.oidcFlows, flowID)
		flow.cancel()
		return PingCAPAuthStatus{Status: "completed", Result: &result}, nil
	}
	if flow.err != "" {
		message := flow.err
		delete(m.oidcFlows, flowID)
		flow.cancel()
		return PingCAPAuthStatus{Status: "error", Error: message}, nil
	}
	return PingCAPAuthStatus{Status: "pending"}, nil
}

// CancelPingCAPAuth releases an unconsumed Desktop OIDC listener immediately.
// Cancellation is idempotent so an unmount racing with completion is harmless.
func (m *Manager) CancelPingCAPAuth(flowID string) {
	flowID = strings.TrimSpace(flowID)
	m.oidcMu.Lock()
	defer m.oidcMu.Unlock()
	if flow, ok := m.oidcFlows[flowID]; ok {
		flow.cancel()
		_ = flow.listener.Close()
		delete(m.oidcFlows, flowID)
	}
}

func (m *Manager) handleOIDCCallback(flowID string) http.HandlerFunc {
	return func(writer http.ResponseWriter, request *http.Request) {
		if request.Method != http.MethodGet {
			writer.WriteHeader(http.StatusMethodNotAllowed)
			return
		}
		query := request.URL.Query()
		state := strings.TrimSpace(query.Get("state"))
		flow, err := m.claimOIDCFlow(flowID, state)
		if err != nil {
			writeOIDCCompletionPage(writer, request, "", false, err.Error())
			return
		}
		if providerError := strings.TrimSpace(query.Get("error")); providerError != "" {
			message := strings.TrimSpace(query.Get("error_description"))
			if message == "" {
				message = providerError
			}
			m.finishOIDCFlow(flowID, nil, message)
			writeOIDCCompletionPage(writer, request, flow.locale, false, message)
			return
		}
		code := strings.TrimSpace(query.Get("code"))
		if code == "" {
			m.finishOIDCFlow(flowID, nil, "The authorization response did not include a code.")
			writeOIDCCompletionPage(writer, request, flow.locale, false, "The authorization response did not include a code.")
			return
		}

		exchangeContext, cancel := context.WithTimeout(flow.ctx, requestTimeout)
		defer cancel()
		var response authSessionResponse
		err = doJSON(exchangeContext, flow.client, http.MethodPost, flow.serverURL+"/api/v1/auth/oidc/callback", map[string]any{
			"code":                                code,
			"state":                               state,
			"nonce":                               flow.nonce,
			"code_verifier":                       flow.verifier,
			"redirect_uri":                        flow.redirectURI,
			"desktopOnboardingEnabled":            flow.desktopOnboardingEnabled,
			"desktopOnboardingRegistrationCutoff": flow.registrationCutoff,
		}, "", &response)
		if err == nil {
			err = exchangeContext.Err()
		}
		if err == nil {
			_, err = m.commitOIDCAuthSession(flowID, exchangeContext, flow.serverURL, flow.client, response)
			if err == nil {
				writeOIDCCompletionPage(writer, request, flow.locale, true, "")
				return
			}
		}
		message := "Unable to complete PingCAP authentication."
		if err != nil && strings.TrimSpace(err.Error()) != "" {
			message = err.Error()
		}
		m.finishOIDCFlow(flowID, nil, message)
		writeOIDCCompletionPage(writer, request, flow.locale, false, message)
	}
}

// commitOIDCAuthSession arbitrates cancellation and credential persistence
// under the same flow-state lock. Once cancellation returns, no callback can
// subsequently enter the durable commit path for that flow.
func (m *Manager) commitOIDCAuthSession(
	flowID string,
	ctx context.Context,
	serverURL string,
	client *http.Client,
	response authSessionResponse,
) (AuthResult, error) {
	if m.oidcPersistHook != nil {
		m.oidcPersistHook()
	}
	m.oidcMu.Lock()
	defer m.oidcMu.Unlock()
	flow, ok := m.oidcFlows[flowID]
	if !ok || flow.ctx.Err() != nil {
		return AuthResult{}, context.Canceled
	}
	result, err := m.persistAuthSession(ctx, serverURL, client, response)
	if err != nil {
		return AuthResult{}, err
	}
	flow.processing = false
	flow.result = &result
	_ = flow.listener.Close()
	return result, nil
}

func (m *Manager) claimOIDCFlow(flowID, state string) (*oidcFlow, error) {
	m.oidcMu.Lock()
	defer m.oidcMu.Unlock()
	m.pruneOIDCFlowsLocked(time.Now())
	flow, ok := m.oidcFlows[flowID]
	if !ok {
		return nil, fmt.Errorf("Desktop login attempt was not found or has expired")
	}
	if flow.processing || flow.result != nil || flow.err != "" {
		return nil, fmt.Errorf("Desktop login attempt has already been used")
	}
	if state == "" || subtle.ConstantTimeCompare([]byte(state), []byte(flow.state)) != 1 {
		flow.err = "The authorization response could not be verified."
		_ = flow.listener.Close()
		return nil, fmt.Errorf("the authorization response could not be verified")
	}
	flow.processing = true
	_ = flow.listener.Close()
	return flow, nil
}

func (m *Manager) finishOIDCFlow(flowID string, result *AuthResult, message string) {
	m.oidcMu.Lock()
	defer m.oidcMu.Unlock()
	flow, ok := m.oidcFlows[flowID]
	if !ok || flow.result != nil || flow.err != "" {
		return
	}
	flow.processing = false
	flow.result = result
	flow.err = strings.TrimSpace(message)
	if result == nil {
		flow.cancel()
	}
	_ = flow.listener.Close()
}

func (m *Manager) expireOIDCFlow(flowID string) {
	m.oidcMu.Lock()
	defer m.oidcMu.Unlock()
	flow, ok := m.oidcFlows[flowID]
	if !ok || time.Now().Before(flow.expiresAt) {
		return
	}
	flow.cancel()
	_ = flow.listener.Close()
	delete(m.oidcFlows, flowID)
}

func (m *Manager) pruneOIDCFlowsLocked(now time.Time) {
	for id, flow := range m.oidcFlows {
		if now.Before(flow.expiresAt) {
			continue
		}
		flow.cancel()
		_ = flow.listener.Close()
		delete(m.oidcFlows, id)
	}
}

func normalizeOIDCCompletionLocale(locale string) string {
	normalized := strings.ToLower(strings.TrimSpace(locale))
	if normalized == "" {
		return "en"
	}
	if normalized == "zh" || strings.HasPrefix(normalized, "zh-") || strings.HasPrefix(normalized, "zh_") {
		return "zh-CN"
	}
	return "en"
}

func oidcCompletionLocale(request *http.Request, flowLocale string) string {
	if strings.TrimSpace(flowLocale) != "" {
		return normalizeOIDCCompletionLocale(flowLocale)
	}
	if request == nil {
		return "en"
	}
	return normalizeOIDCCompletionLocale(request.Header.Get("Accept-Language"))
}

func writeOIDCCompletionPage(writer http.ResponseWriter, request *http.Request, flowLocale string, success bool, message string) {
	locale := oidcCompletionLocale(request, flowLocale)
	lang := "en"
	title := "Sign-in did not finish"
	detail := strings.TrimSpace(message)
	button := "Return to desktop app"
	state := "Return to the desktop app and try again."
	if locale == "zh-CN" {
		lang = "zh-CN"
		title = "登录未完成"
		button = "返回桌面端"
		state = "登录没有完成，请回到桌面端后重试。"
	}
	if detail == "" {
		if locale == "zh-CN" {
			detail = "无法完成登录，请回到桌面端后重试。"
		} else {
			detail = "Unable to complete sign-in. Return to the desktop app and try again."
		}
	}
	if strings.EqualFold(detail, http.StatusText(http.StatusBadGateway)) {
		if locale == "zh-CN" {
			detail = "AgentTeam Server 暂时无法完成登录 (HTTP 502)，请稍后重试。"
		} else {
			detail = "AgentTeam Server could not complete sign-in right now (HTTP 502). Try again shortly."
		}
	}
	action := fmt.Sprintf("<a class=\"button button-secondary\" href=\"%s\">%s</a>", html.EscapeString(oidcDesktopReturnURL), html.EscapeString(button))
	bodyClass := "is-failed"
	if success {
		bodyClass = "is-complete"
		state = "You can close this browser tab after the desktop app opens."
		if locale == "zh-CN" {
			title = "已完成登录"
			detail = "桌面端应该会自动打开。如果没有，请点击下面的按钮返回。"
			state = "桌面端打开后，可以关闭这个浏览器标签页。"
		} else {
			title = "You are signed in"
			detail = "The desktop app should reopen automatically. If it does not, use the button below."
		}
		action = fmt.Sprintf("<a class=\"button\" href=\"%s\">%s</a><script>window.location.href=%q;</script>", html.EscapeString(oidcDesktopReturnURL), html.EscapeString(button), oidcDesktopReturnURL)
	}
	writer.Header().Set("Content-Type", "text/html; charset=utf-8")
	writer.Header().Set("Cache-Control", "no-store")
	writer.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; base-uri 'none'; form-action 'none'")
	page := strings.NewReplacer(
		"{{LANG}}", html.EscapeString(lang),
		"{{TITLE}}", html.EscapeString(title),
		"{{BODY_CLASS}}", html.EscapeString(bodyClass),
		"{{DETAIL}}", html.EscapeString(detail),
		"{{ACTION}}", action,
		"{{STATE}}", html.EscapeString(state),
	).Replace(`<!doctype html>
<html lang="{{LANG}}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{{TITLE}}</title>
<style>
:root{color-scheme:dark}
*{box-sizing:border-box}
html,body{min-height:100%}
body{margin:0;background:#070707;color:#f4f4f5;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;letter-spacing:0}
body:before{content:"";position:fixed;inset:0;background-image:radial-gradient(circle at 1px 1px,rgba(255,255,255,.12) 1px,transparent 0),linear-gradient(180deg,rgba(255,255,255,.055),rgba(255,255,255,0) 34%);background-size:18px 18px,100% 100%;opacity:.42;pointer-events:none}
body:after{content:"";position:fixed;inset:0;background:radial-gradient(circle at 50% 36%,rgba(255,255,255,.08),transparent 24rem),linear-gradient(180deg,rgba(7,7,7,0),#070707 76%);pointer-events:none}
.page{position:relative;z-index:1;display:grid;min-height:100vh;place-items:center;padding:clamp(24px,6vw,72px);text-align:center}
.content{display:flex;width:min(680px,100%);min-height:min(560px,calc(100vh - 48px));flex-direction:column;align-items:center;justify-content:center}
.brand-mark{display:inline-flex;height:42px;min-width:42px;align-items:center;justify-content:center;border:1px solid rgba(255,255,255,.18);border-radius:12px;background:rgba(255,255,255,.08);color:#fff;font-size:14px;font-weight:740;letter-spacing:0;text-transform:none}
h1{max-width:620px;margin:26px 0 0;color:#fff;font-size:clamp(30px,3.6vw,44px);font-weight:700;line-height:1.12;letter-spacing:0}
.detail{max-width:560px;margin:18px auto 0;color:rgba(244,244,245,.7);font-size:clamp(15px,1.35vw,18px);line-height:1.58}
.actions{display:flex;flex-wrap:wrap;justify-content:center;gap:12px;margin-top:34px}
.button{display:inline-flex;min-height:46px;align-items:center;justify-content:center;border:1px solid rgba(255,255,255,.22);border-radius:10px;background:#f4f4f5;color:#09090b;padding:0 18px;text-decoration:none;font-size:15px;font-weight:740;box-shadow:0 16px 44px rgba(0,0,0,.28)}
.button-secondary{background:rgba(255,255,255,.09);color:#f4f4f5;box-shadow:none}
.button:focus-visible{outline:3px solid rgba(255,255,255,.45);outline-offset:3px}
.state{max-width:620px;margin:18px auto 0;color:rgba(244,244,245,.48);font-size:14px;line-height:1.7}
@media (max-width:640px){.page{padding:24px}.content{min-height:calc(100vh - 48px)}h1{font-size:34px}.detail{font-size:16px}.button{width:100%}}
</style>
</head>
<body class="{{BODY_CLASS}}">
<div class="page">
<main class="content">
<span class="brand-mark">AgentTeam</span>
<h1>{{TITLE}}</h1>
<p class="detail">{{DETAIL}}</p>
<div class="actions">{{ACTION}}</div>
<p class="state">{{STATE}}</p>
</main>
</div>
</body>
</html>`)
	_, _ = writer.Write([]byte(page))
}
