package handler

import (
	"errors"
	"fmt"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"

	"github.com/warmbly/warmbly/internal/api/middleware"
	"github.com/warmbly/warmbly/internal/app/oauth"
	"github.com/warmbly/warmbly/internal/errx"
	"github.com/warmbly/warmbly/internal/models"
	"github.com/warmbly/warmbly/internal/utils/paging"
)

// respondOAuthError renders an *oauth.OAuthError as its RFC 6749 JSON body, or a
// generic invalid_request for anything else.
func respondOAuthError(c *gin.Context, err error) {
	var oe *oauth.OAuthError
	if errors.As(err, &oe) {
		c.JSON(oe.HTTPStatus, oe)
		return
	}
	c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request", "error_description": err.Error()})
}

// --- Application management (JWT + org gate) --------------------------------

func (h *Handler) ListOAuthApplications(c *gin.Context) {
	orgID := middleware.GetOrganizationID(c)
	if orgID == nil {
		errx.JSON(c, errx.New(errx.BadRequest, "no organization selected"))
		return
	}
	apps, err := h.OAuthService.ListApplications(c.Request.Context(), *orgID)
	if err != nil {
		errx.JSON(c, errx.New(errx.Internal, "lookup failed"))
		return
	}
	c.JSON(http.StatusOK, gin.H{"applications": apps})
}

func (h *Handler) CreateOAuthApplication(c *gin.Context) {
	orgID := middleware.GetOrganizationID(c)
	if orgID == nil {
		errx.JSON(c, errx.New(errx.BadRequest, "no organization selected"))
		return
	}
	userID, uerr := middleware.GetUserUUID(c)
	if uerr != nil {
		errx.JSON(c, errx.ErrUnauthorized)
		return
	}
	var w models.OAuthApplicationWrite
	if err := c.ShouldBindJSON(&w); err != nil {
		errx.JSON(c, errx.New(errx.BadRequest, "invalid request body"))
		return
	}
	app, err := h.OAuthService.RegisterApplication(c.Request.Context(), *orgID, userID, w)
	if err != nil {
		errx.JSON(c, errx.New(errx.BadRequest, err.Error()))
		return
	}
	c.JSON(http.StatusCreated, app)
}

func (h *Handler) GetOAuthApplication(c *gin.Context) {
	orgID := middleware.GetOrganizationID(c)
	if orgID == nil {
		errx.JSON(c, errx.New(errx.BadRequest, "no organization selected"))
		return
	}
	id, err := uuid.Parse(c.Param("id"))
	if err != nil {
		errx.JSON(c, errx.New(errx.BadRequest, "invalid id"))
		return
	}
	app, gerr := h.OAuthService.GetApplication(c.Request.Context(), *orgID, id)
	if gerr != nil {
		errx.JSON(c, errx.New(errx.Internal, "lookup failed"))
		return
	}
	if app == nil {
		errx.JSON(c, errx.New(errx.NotFound, "application not found"))
		return
	}
	c.JSON(http.StatusOK, app)
}

func (h *Handler) UpdateOAuthApplication(c *gin.Context) {
	orgID := middleware.GetOrganizationID(c)
	if orgID == nil {
		errx.JSON(c, errx.New(errx.BadRequest, "no organization selected"))
		return
	}
	id, err := uuid.Parse(c.Param("id"))
	if err != nil {
		errx.JSON(c, errx.New(errx.BadRequest, "invalid id"))
		return
	}
	var w models.OAuthApplicationWrite
	if err := c.ShouldBindJSON(&w); err != nil {
		errx.JSON(c, errx.New(errx.BadRequest, "invalid request body"))
		return
	}
	app, uerr := h.OAuthService.UpdateApplication(c.Request.Context(), *orgID, id, w)
	if uerr != nil {
		errx.JSON(c, errx.New(errx.BadRequest, uerr.Error()))
		return
	}
	c.JSON(http.StatusOK, app)
}

func (h *Handler) DeleteOAuthApplication(c *gin.Context) {
	orgID := middleware.GetOrganizationID(c)
	if orgID == nil {
		errx.JSON(c, errx.New(errx.BadRequest, "no organization selected"))
		return
	}
	id, err := uuid.Parse(c.Param("id"))
	if err != nil {
		errx.JSON(c, errx.New(errx.BadRequest, "invalid id"))
		return
	}
	if derr := h.OAuthService.DeleteApplication(c.Request.Context(), *orgID, id); derr != nil {
		errx.JSON(c, errx.New(errx.Internal, "delete failed"))
		return
	}
	c.JSON(http.StatusOK, gin.H{"deleted": true})
}

func (h *Handler) RotateOAuthApplicationSecret(c *gin.Context) {
	orgID := middleware.GetOrganizationID(c)
	if orgID == nil {
		errx.JSON(c, errx.New(errx.BadRequest, "no organization selected"))
		return
	}
	id, err := uuid.Parse(c.Param("id"))
	if err != nil {
		errx.JSON(c, errx.New(errx.BadRequest, "invalid id"))
		return
	}
	secret, rerr := h.OAuthService.RotateSecret(c.Request.Context(), *orgID, id)
	if rerr != nil {
		errx.JSON(c, errx.New(errx.BadRequest, rerr.Error()))
		return
	}
	c.JSON(http.StatusOK, gin.H{"client_secret": secret})
}

// GetOAuthAppWebhookSecret reveals the app's webhook signing secret (used to
// verify every app-webhook delivery, from any org).
//
// GET /oauth/applications/:id/webhook-secret
func (h *Handler) GetOAuthAppWebhookSecret(c *gin.Context) {
	orgID := middleware.GetOrganizationID(c)
	if orgID == nil {
		errx.JSON(c, errx.New(errx.BadRequest, "no organization selected"))
		return
	}
	id, err := uuid.Parse(c.Param("id"))
	if err != nil {
		errx.JSON(c, errx.New(errx.BadRequest, "invalid id"))
		return
	}
	secret, rerr := h.OAuthService.WebhookSecret(c.Request.Context(), *orgID, id)
	if rerr != nil {
		errx.JSON(c, errx.New(errx.NotFound, rerr.Error()))
		return
	}
	c.JSON(http.StatusOK, gin.H{"webhook_secret": secret})
}

// RotateOAuthAppWebhookSecret mints a new app webhook signing secret and
// re-materializes every install with it.
//
// POST /oauth/applications/:id/webhook-secret/rotate
func (h *Handler) RotateOAuthAppWebhookSecret(c *gin.Context) {
	orgID := middleware.GetOrganizationID(c)
	if orgID == nil {
		errx.JSON(c, errx.New(errx.BadRequest, "no organization selected"))
		return
	}
	id, err := uuid.Parse(c.Param("id"))
	if err != nil {
		errx.JSON(c, errx.New(errx.BadRequest, "invalid id"))
		return
	}
	secret, rerr := h.OAuthService.RotateWebhookSecret(c.Request.Context(), *orgID, id)
	if rerr != nil {
		errx.JSON(c, errx.New(errx.BadRequest, rerr.Error()))
		return
	}
	c.JSON(http.StatusOK, gin.H{"webhook_secret": secret})
}

// ListOAuthAppWebhookEndpoints returns the per-org endpoints the app has
// materialized (every org that authorized it), with health.
//
// GET /oauth/applications/:id/webhook-endpoints
func (h *Handler) ListOAuthAppWebhookEndpoints(c *gin.Context) {
	orgID := middleware.GetOrganizationID(c)
	if orgID == nil {
		errx.JSON(c, errx.New(errx.BadRequest, "no organization selected"))
		return
	}
	id, err := uuid.Parse(c.Param("id"))
	if err != nil {
		errx.JSON(c, errx.New(errx.BadRequest, "invalid id"))
		return
	}
	app, aerr := h.OAuthService.GetApplication(c.Request.Context(), *orgID, id)
	if aerr != nil || app == nil {
		errx.JSON(c, errx.New(errx.NotFound, "application not found"))
		return
	}
	eps, lerr := h.WebhookService.ListAppEndpoints(c.Request.Context(), id)
	if lerr != nil {
		errx.JSON(c, errx.New(errx.Internal, "failed to list endpoints"))
		return
	}
	c.JSON(http.StatusOK, gin.H{"endpoints": eps})
}

// ListOAuthAppWebhookDeliveries returns the app-developer delivery log: every
// attempt across all of the app's endpoints, filterable + paginated.
//
// GET /oauth/applications/:id/webhook-deliveries
func (h *Handler) ListOAuthAppWebhookDeliveries(c *gin.Context) {
	orgID := middleware.GetOrganizationID(c)
	if orgID == nil {
		errx.JSON(c, errx.New(errx.BadRequest, "no organization selected"))
		return
	}
	id, err := uuid.Parse(c.Param("id"))
	if err != nil {
		errx.JSON(c, errx.New(errx.BadRequest, "invalid id"))
		return
	}
	app, aerr := h.OAuthService.GetApplication(c.Request.Context(), *orgID, id)
	if aerr != nil || app == nil {
		errx.JSON(c, errx.New(errx.NotFound, "application not found"))
		return
	}
	filter := models.WebhookDeliveryFilter{
		Status:    c.Query("status"),
		EventType: c.Query("event_type"),
		Limit:     50,
	}
	if raw := c.Query("limit"); raw != "" {
		n, perr := strconv.Atoi(raw)
		if perr != nil || n <= 0 || n > 200 {
			errx.JSON(c, errx.New(errx.BadRequest, "limit must be between 1 and 200"))
			return
		}
		filter.Limit = n
	}
	offset, xerr := paging.DecodeOffsetCursor(c.Query("cursor"))
	if xerr != nil {
		errx.Handle(c, xerr)
		return
	}
	filter.Offset = offset
	result, lerr := h.WebhookService.ListAppDeliveries(c.Request.Context(), id, filter)
	if lerr != nil {
		errx.JSON(c, errx.New(errx.Internal, "failed to list deliveries"))
		return
	}
	c.JSON(http.StatusOK, result)
}

// UploadOAuthAppLogo stores an app logo image (PNG/JPG, <=2MB) in public object
// storage and returns its URL. Used by the registration UI during the branding
// step: there is no app id yet, so the returned URL is sent in the create/update
// payload. Reuses the avatar upload validation + storage path.
func (h *Handler) UploadOAuthAppLogo(c *gin.Context) {
	orgID := middleware.GetOrganizationID(c)
	if orgID == nil {
		errx.JSON(c, errx.New(errx.BadRequest, "no organization selected"))
		return
	}
	body, mime, ext, xerr := readAvatarUpload(c)
	if xerr != nil {
		errx.JSON(c, xerr)
		return
	}
	key := fmt.Sprintf("oauth-app-logos/%s-%d%s", orgID.String(), time.Now().Unix(), ext)
	url, xerr := putPublicObject(c.Request.Context(), h.Storage, key, body, mime)
	if xerr != nil {
		errx.JSON(c, xerr)
		return
	}
	c.JSON(http.StatusOK, gin.H{"logo_url": url})
}

// --- Authorize (JWT: the logged-in user consents) ---------------------------

func authorizeRequestFrom(get func(string) string) oauth.AuthorizeRequest {
	return oauth.AuthorizeRequest{
		ResponseType:        get("response_type"),
		ClientID:            get("client_id"),
		RedirectURI:         get("redirect_uri"),
		Scope:               get("scope"),
		State:               get("state"),
		CodeChallenge:       get("code_challenge"),
		CodeChallengeMethod: get("code_challenge_method"),
	}
}

// OAuthAuthorizeDetails returns the consent info the dashboard renders before
// asking the user to approve. Read from query params.
func (h *Handler) OAuthAuthorizeDetails(c *gin.Context) {
	req := authorizeRequestFrom(c.Query)
	info, err := h.OAuthService.AuthorizeDetails(c.Request.Context(), req)
	if err != nil {
		respondOAuthError(c, err)
		return
	}
	c.JSON(http.StatusOK, info)
}

// OAuthAuthorize is called when the user approves consent. It mints the code and
// returns the redirect URL the browser should follow back to the app.
func (h *Handler) OAuthAuthorize(c *gin.Context) {
	orgID := middleware.GetOrganizationID(c)
	if orgID == nil {
		errx.JSON(c, errx.New(errx.BadRequest, "no organization selected"))
		return
	}
	userID, uerr := middleware.GetUserUUID(c)
	if uerr != nil {
		errx.JSON(c, errx.ErrUnauthorized)
		return
	}
	var body struct {
		ResponseType        string `json:"response_type"`
		ClientID            string `json:"client_id"`
		RedirectURI         string `json:"redirect_uri"`
		Scope               string `json:"scope"`
		State               string `json:"state"`
		CodeChallenge       string `json:"code_challenge"`
		CodeChallengeMethod string `json:"code_challenge_method"`
	}
	if err := c.ShouldBindJSON(&body); err != nil {
		errx.JSON(c, errx.New(errx.BadRequest, "invalid request body"))
		return
	}
	req := oauth.AuthorizeRequest{
		ResponseType:        body.ResponseType,
		ClientID:            body.ClientID,
		RedirectURI:         body.RedirectURI,
		Scope:               body.Scope,
		State:               body.State,
		CodeChallenge:       body.CodeChallenge,
		CodeChallengeMethod: body.CodeChallengeMethod,
	}
	redirect, err := h.OAuthService.IssueAuthorizationCode(c.Request.Context(), *orgID, userID, req)
	if err != nil {
		respondOAuthError(c, err)
		return
	}
	c.JSON(http.StatusOK, gin.H{"redirect_url": redirect})
}

// --- Token + revoke (public; client-authenticated) --------------------------

// clientCredentials pulls client_id/secret from HTTP Basic auth (preferred) or
// the form body, per RFC 6749 §2.3.1.
func clientCredentials(c *gin.Context) (string, string) {
	if id, secret, ok := c.Request.BasicAuth(); ok {
		return id, secret
	}
	return c.PostForm("client_id"), c.PostForm("client_secret")
}

// OAuthToken is the token endpoint. It dispatches on grant_type.
func (h *Handler) OAuthToken(c *gin.Context) {
	grantType := c.PostForm("grant_type")
	clientID, clientSecret := clientCredentials(c)
	switch grantType {
	case "authorization_code":
		resp, err := h.OAuthService.ExchangeCode(
			c.Request.Context(), clientID, clientSecret,
			c.PostForm("code"), c.PostForm("redirect_uri"), c.PostForm("code_verifier"),
		)
		if err != nil {
			respondOAuthError(c, err)
			return
		}
		c.Header("Cache-Control", "no-store")
		c.JSON(http.StatusOK, resp)
	case "refresh_token":
		resp, err := h.OAuthService.RefreshToken(c.Request.Context(), clientID, clientSecret, c.PostForm("refresh_token"))
		if err != nil {
			respondOAuthError(c, err)
			return
		}
		c.Header("Cache-Control", "no-store")
		c.JSON(http.StatusOK, resp)
	default:
		c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported_grant_type", "error_description": "grant_type must be authorization_code or refresh_token"})
	}
}

// OAuthRevoke is the token-revocation endpoint (RFC 7009). It succeeds even for
// an unknown token, so the response never confirms a token's existence.
func (h *Handler) OAuthRevoke(c *gin.Context) {
	clientID, clientSecret := clientCredentials(c)
	token := c.PostForm("token")
	if err := h.OAuthService.RevokeToken(c.Request.Context(), clientID, clientSecret, token); err != nil {
		respondOAuthError(c, err)
		return
	}
	c.JSON(http.StatusOK, gin.H{"revoked": true})
}

// --- Authorized apps (JWT: the user's connected apps) -----------------------

func (h *Handler) ListAuthorizedApps(c *gin.Context) {
	orgID := middleware.GetOrganizationID(c)
	if orgID == nil {
		errx.JSON(c, errx.New(errx.BadRequest, "no organization selected"))
		return
	}
	userID, uerr := middleware.GetUserUUID(c)
	if uerr != nil {
		errx.JSON(c, errx.ErrUnauthorized)
		return
	}
	apps, err := h.OAuthService.ListAuthorizedApps(c.Request.Context(), *orgID, userID)
	if err != nil {
		errx.JSON(c, errx.New(errx.Internal, "lookup failed"))
		return
	}
	c.JSON(http.StatusOK, gin.H{"authorized_apps": apps})
}

func (h *Handler) RevokeAuthorizedApp(c *gin.Context) {
	orgID := middleware.GetOrganizationID(c)
	if orgID == nil {
		errx.JSON(c, errx.New(errx.BadRequest, "no organization selected"))
		return
	}
	userID, uerr := middleware.GetUserUUID(c)
	if uerr != nil {
		errx.JSON(c, errx.ErrUnauthorized)
		return
	}
	appID, err := uuid.Parse(c.Param("id"))
	if err != nil {
		errx.JSON(c, errx.New(errx.BadRequest, "invalid id"))
		return
	}
	if rerr := h.OAuthService.RevokeAuthorization(c.Request.Context(), *orgID, userID, appID); rerr != nil {
		errx.JSON(c, errx.New(errx.Internal, "revoke failed"))
		return
	}
	c.JSON(http.StatusOK, gin.H{"revoked": true})
}

// --- Dynamic Client Registration (RFC 7591) ---------------------------------

// OAuthRegisterClient is the open (unauthenticated) client-registration endpoint.
// MCP clients call it to self-register as a public (PKCE, no-secret) client so a
// customer can connect with one `claude mcp add` and a browser sign-in. Per-IP
// rate-limited in the service; registration mints only a client_id and grants no
// access on its own (a human still approves scopes at consent).
//
// POST /v1/oauth/register
func (h *Handler) OAuthRegisterClient(c *gin.Context) {
	var req oauth.DCRRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_client_metadata", "error_description": "invalid request body"})
		return
	}
	resp, err := h.OAuthService.RegisterDynamicClient(c.Request.Context(), c.ClientIP(), req)
	if err != nil {
		respondOAuthError(c, err)
		return
	}
	c.Header("Cache-Control", "no-store")
	c.JSON(http.StatusCreated, resp)
}

// --- Discovery (RFC 8414 + RFC 9728) ----------------------------------------

// publicAPIBaseURL is the absolute base of this API (issuer + resource origin),
// from API_PUBLIC_URL, falling back to the request host. No trailing slash.
func publicAPIBaseURL(c *gin.Context) string {
	apiURL := strings.TrimRight(os.Getenv("API_PUBLIC_URL"), "/")
	if apiURL == "" {
		scheme := "https"
		if c.Request.TLS == nil && c.Request.Header.Get("X-Forwarded-Proto") == "" {
			scheme = "http"
		}
		apiURL = scheme + "://" + c.Request.Host
	}
	return apiURL
}

// OAuthServerMetadata advertises the authorization server's endpoints and
// capabilities. The authorization endpoint is the dashboard consent page; token,
// revocation, and registration are this API. APP_URL/API_PUBLIC_URL drive the URLs.
func (h *Handler) OAuthServerMetadata(c *gin.Context) {
	appURL := strings.TrimRight(os.Getenv("APP_URL"), "/")
	apiURL := publicAPIBaseURL(c)
	c.JSON(http.StatusOK, gin.H{
		"issuer":                                apiURL,
		"authorization_endpoint":                appURL + "/oauth/authorize",
		"token_endpoint":                        apiURL + "/v1/oauth/token",
		"revocation_endpoint":                   apiURL + "/v1/oauth/revoke",
		"registration_endpoint":                 apiURL + "/v1/oauth/register",
		"response_types_supported":              []string{"code"},
		"grant_types_supported":                 []string{"authorization_code", "refresh_token"},
		"code_challenge_methods_supported":      []string{"S256"},
		"token_endpoint_auth_methods_supported": []string{"none", "client_secret_basic", "client_secret_post"},
		"scopes_supported":                      oauth.ScopeList(models.AllAPIPermissionsMask),
	})
}

// OAuthProtectedResourceMetadata is the RFC 9728 protected-resource metadata for
// the MCP endpoint. An MCP client discovers it from the WWW-Authenticate challenge
// on /v1/mcp, reads the authorization server from here, then runs the OAuth flow.
//
// GET /.well-known/oauth-protected-resource (and the /v1/mcp path-suffixed form)
func (h *Handler) OAuthProtectedResourceMetadata(c *gin.Context) {
	apiURL := publicAPIBaseURL(c)
	c.JSON(http.StatusOK, gin.H{
		"resource":                 apiURL + "/v1/mcp",
		"authorization_servers":    []string{apiURL},
		"scopes_supported":         oauth.ScopeList(oauth.MCPRegistrableScopes()),
		"bearer_methods_supported": []string{"header"},
		"resource_documentation":   "https://docs.warmbly.com/api/mcp/",
	})
}
