package server

import (
	"net/http"

	"git.wntrmute.dev/mc/mcias/internal/audit"
	"git.wntrmute.dev/mc/mcias/internal/middleware"
	"git.wntrmute.dev/mc/mcias/internal/model"
	"git.wntrmute.dev/mc/mcias/internal/policy"
	"git.wntrmute.dev/mc/mcias/internal/sso"
	"git.wntrmute.dev/mc/mcias/internal/token"
)

// ssoTokenRequest is the request body for POST /v1/sso/token.
type ssoTokenRequest struct {
	Code        string `json:"code"`
	ClientID    string `json:"client_id"`
	RedirectURI string `json:"redirect_uri"`
}

// handleSSOTokenExchange exchanges an SSO authorization code for a JWT token.
//
// Security design:
//   - The authorization code is single-use (consumed via LoadAndDelete).
//   - The client_id and redirect_uri must match the values stored when the code
//     was issued, preventing a stolen code from being exchanged by a different
//     service.
//   - Policy evaluation uses the service_name and tags from the registered SSO
//     client config (not from the request), preventing identity spoofing.
//   - The code expires after 60 seconds to limit the interception window.
func (s *Server) handleSSOTokenExchange(w http.ResponseWriter, r *http.Request) {
	var req ssoTokenRequest
	if !decodeJSON(w, r, &req) {
		return
	}

	if req.Code == "" || req.ClientID == "" || req.RedirectURI == "" {
		middleware.WriteError(w, http.StatusBadRequest, "code, client_id, and redirect_uri are required", "bad_request")
		return
	}

	// Consume the authorization code (single-use).
	ac, ok := sso.Consume(req.Code)
	if !ok {
		middleware.WriteError(w, http.StatusUnauthorized, "invalid or expired authorization code", "invalid_code")
		return
	}

	// Security: verify client_id and redirect_uri match the stored values.
	if ac.ClientID != req.ClientID || ac.RedirectURI != req.RedirectURI {
		s.logger.Warn("sso: token exchange parameter mismatch",
			"expected_client", ac.ClientID, "got_client", req.ClientID)
		middleware.WriteError(w, http.StatusUnauthorized, "invalid or expired authorization code", "invalid_code")
		return
	}

	// Look up the registered SSO client from the database for policy context.
	client, clientErr := s.db.GetSSOClient(req.ClientID)
	if clientErr != nil {
		// Should not happen if the authorize endpoint validated, but defend in depth.
		middleware.WriteError(w, http.StatusUnauthorized, "unknown client", "invalid_code")
		return
	}
	if !client.Enabled {
		middleware.WriteError(w, http.StatusForbidden, "SSO client is disabled", "client_disabled")
		return
	}

	// Load account.
	acct, err := s.db.GetAccountByID(ac.AccountID)
	if err != nil {
		s.logger.Error("sso: load account for token exchange", "error", err, "account_id", ac.AccountID)
		middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
		return
	}

	if acct.Status != model.AccountStatusActive {
		middleware.WriteError(w, http.StatusForbidden, "account is not active", "account_inactive")
		return
	}

	// Load roles for policy evaluation and expiry decision.
	roles, err := s.db.GetRoles(acct.ID)
	if err != nil {
		middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
		return
	}

	// Policy evaluation: client_id serves as both identifier and service_name.
	{
		input := policy.PolicyInput{
			Subject:     acct.UUID,
			AccountType: string(acct.AccountType),
			Roles:       roles,
			Action:      policy.ActionLogin,
			Resource: policy.Resource{
				ServiceName: client.ClientID,
				Tags:        client.Tags,
			},
		}
		if effect, _ := s.polEng.Evaluate(input); effect == policy.Deny {
			s.writeAudit(r, model.EventLoginFail, &acct.ID, nil,
				audit.JSON("reason", "policy_deny", "service_name", client.ClientID, "via", "sso"))
			middleware.WriteError(w, http.StatusForbidden, "access denied by policy", "policy_denied")
			return
		}
	}

	// Determine expiry.
	expiry := s.cfg.DefaultExpiry()
	for _, rol := range roles {
		if rol == "admin" {
			expiry = s.cfg.AdminExpiry()
			break
		}
	}

	privKey, err := s.vault.PrivKey()
	if err != nil {
		middleware.WriteError(w, http.StatusServiceUnavailable, "vault sealed", "vault_sealed")
		return
	}
	tokenStr, claims, err := token.IssueToken(privKey, s.cfg.Tokens.Issuer, acct.UUID, roles, expiry)
	if err != nil {
		s.logger.Error("sso: issue token", "error", err)
		middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
		return
	}

	if err := s.db.TrackToken(claims.JTI, acct.ID, claims.IssuedAt, claims.ExpiresAt); err != nil {
		s.logger.Error("sso: track token", "error", err)
		middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
		return
	}

	s.writeAudit(r, model.EventSSOLoginOK, &acct.ID, nil,
		audit.JSON("jti", claims.JTI, "client_id", client.ClientID))
	s.writeAudit(r, model.EventTokenIssued, &acct.ID, nil,
		audit.JSON("jti", claims.JTI, "via", "sso"))

	writeJSON(w, http.StatusOK, loginResponse{
		Token:     tokenStr,
		ExpiresAt: claims.ExpiresAt.Format("2006-01-02T15:04:05Z"),
	})
}
