package httpx

import (
	"errors"
	"net/http"
	"net/url"
	"strings"

	"bountyboard/internal/domain"
	"bountyboard/internal/store"
	"bountyboard/internal/ulid"
)

const (
	oidcStateCookie    = "bb_oidc_state"
	oidcVerifierCookie = "bb_oidc_verifier"
	oidcCookieTTL      = 300 // seconds; the hop to the IdP and back
)

func (s *Server) oidcFlowCookie(name, value string) *http.Cookie {
	maxAge := oidcCookieTTL
	if value == "" {
		maxAge = -1
	}
	return &http.Cookie{
		Name: name, Value: value, Path: "/api/v1/auth/oidc",
		MaxAge: maxAge, HttpOnly: true, Secure: s.cfg.CookieSecure(),
		SameSite: http.SameSiteLaxMode,
	}
}

func (s *Server) handleOIDCLogin(w http.ResponseWriter, r *http.Request) {
	if !s.oidc.Enabled() {
		writeError(w, http.StatusNotFound, "oidc_disabled", "SSO is not configured")
		return
	}
	authURL, state, verifier, err := s.oidc.AuthURL(r.Context())
	if err != nil {
		s.internalError(w, r, "oidc auth url", err)
		return
	}
	http.SetCookie(w, s.oidcFlowCookie(oidcStateCookie, state))
	http.SetCookie(w, s.oidcFlowCookie(oidcVerifierCookie, verifier))
	http.Redirect(w, r, authURL, http.StatusFound)
}

// loginRedirect sends browser-flow errors back to the login page (the API
// has no JSON consumer mid-redirect).
func (s *Server) loginRedirect(w http.ResponseWriter, r *http.Request, errCode string) {
	target := "/login"
	if errCode != "" {
		target += "?error=" + url.QueryEscape(errCode)
	}
	http.Redirect(w, r, target, http.StatusFound)
}

func (s *Server) handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
	if !s.oidc.Enabled() {
		writeError(w, http.StatusNotFound, "oidc_disabled", "SSO is not configured")
		return
	}
	// Always clear the one-shot flow cookies.
	defer func() {
		http.SetCookie(w, s.oidcFlowCookie(oidcStateCookie, ""))
		http.SetCookie(w, s.oidcFlowCookie(oidcVerifierCookie, ""))
	}()

	stateCookie, err1 := r.Cookie(oidcStateCookie)
	verifierCookie, err2 := r.Cookie(oidcVerifierCookie)
	if err1 != nil || err2 != nil || stateCookie.Value == "" ||
		r.URL.Query().Get("state") != stateCookie.Value {
		s.loginRedirect(w, r, "oidc_state_mismatch")
		return
	}
	code := r.URL.Query().Get("code")
	if code == "" {
		s.loginRedirect(w, r, "oidc_denied")
		return
	}

	claims, err := s.oidc.Exchange(r.Context(), code, verifierCookie.Value)
	if err != nil {
		s.log.Error("oidc exchange", "err", err, "requestId", RequestID(r.Context()))
		s.loginRedirect(w, r, "oidc_failed")
		return
	}

	u, err := s.resolveOIDCUser(r, claims.Issuer, claims.Subject, claims.Email, claims.EmailVerified, claims.Name)
	if err != nil {
		var code *oidcLoginError
		if errors.As(err, &code) {
			s.loginRedirect(w, r, code.code)
			return
		}
		s.internalError(w, r, "oidc user resolution", err)
		return
	}

	sess, csrf, err := s.newSessionFor(r, u)
	if err != nil {
		s.internalError(w, r, "create session", err)
		return
	}
	s.setAuthCookies(w, sess.Token, csrf)
	http.Redirect(w, r, "/", http.StatusFound)
}

type oidcLoginError struct{ code string }

func (e *oidcLoginError) Error() string { return "oidc login rejected: " + e.code }

// resolveOIDCUser implements §7 linking: by (issuer,subject) first, then by
// verified email, otherwise a fresh developer account.
func (s *Server) resolveOIDCUser(r *http.Request, issuer, subject, email string, emailVerified bool, name string) (*domain.User, error) {
	ctx := r.Context()

	u, err := s.store.UserByOIDC(ctx, issuer, subject)
	switch {
	case err == nil:
		if u.Disabled {
			return nil, &oidcLoginError{code: "account_disabled"}
		}
		return u, nil
	case !errors.Is(err, store.ErrNotFound):
		return nil, err
	}

	email = store.NormalizeEmail(email)
	if email == "" {
		return nil, &oidcLoginError{code: "oidc_no_email"}
	}

	existing, err := s.store.UserByEmail(ctx, email)
	switch {
	case err == nil:
		// Linking an SSO identity to a pre-existing local account requires
		// the IdP to vouch for the address, otherwise anyone controlling the
		// IdP account could take over the local one.
		if !emailVerified {
			return nil, &oidcLoginError{code: "oidc_email_unverified"}
		}
		if existing.Disabled {
			return nil, &oidcLoginError{code: "account_disabled"}
		}
		if err := s.store.LinkOIDC(ctx, existing.ID, issuer, subject); err != nil {
			return nil, err
		}
		existing.Auth.OIDC = &domain.OIDCAuth{Issuer: issuer, Subject: subject}
		return existing, nil
	case !errors.Is(err, store.ErrNotFound):
		return nil, err
	}

	if strings.TrimSpace(name) == "" {
		name = email
	}
	u = &domain.User{
		ID:    ulid.New(),
		Email: email,
		Name:  name,
		Roles: domain.Roles{Developer: true}, // first OIDC login = developer (§7)
		Auth:  domain.Auth{OIDC: &domain.OIDCAuth{Issuer: issuer, Subject: subject}},
		Settings: domain.UserSettings{
			Theme:         "light",
			Notifications: domain.NotificationPrefs{Email: true, InApp: true},
		},
	}
	if err := s.store.CreateUser(ctx, u); err != nil {
		return nil, err
	}
	s.metrics.Inc("auth_oidc_signups_total", 1)
	return u, nil
}
