package zitadel

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
)

const fieldPassword = "password"

// Client calls ZITADEL Management API v2 and OAuth token endpoints.
type Client struct {
	conf    Conf
	http    *http.Client
	apiBase string
	issuer  string
	jwks    *jwksCache
}

// NewClient constructs a Client. Returns (nil, nil) when Issuer is empty.
func NewClient(conf Conf) (*Client, error) {
	conf = conf.Defaults()
	if conf.Issuer == "" {
		return nil, nil
	}
	apiBase := strings.TrimRight(conf.APIBase, "/")
	issuer := strings.TrimRight(conf.Issuer, "/")
	if apiBase == "" {
		apiBase = issuer
	}
	return &Client{
		conf:    conf,
		apiBase: apiBase,
		issuer:  issuer,
		http: &http.Client{
			Timeout: conf.timeout(),
		},
	}, nil
}

func (c *Client) postToken(ctx context.Context, form url.Values) (*TokenResult, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.issuer+"/oauth/v2/token", strings.NewReader(form.Encode()))
	if err != nil {
		return nil, fmt.Errorf("zitadel: token request: %w", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("Accept", "application/json")

	resp, err := c.http.Do(req)
	if err != nil {
		return nil, fmt.Errorf("zitadel: token request: %w", err)
	}
	defer resp.Body.Close()

	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("zitadel: read token response: %w", err)
	}
	if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusBadRequest {
		return nil, ErrInvalidCredentials
	}
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("zitadel: token request: status %d: %s", resp.StatusCode, truncateBody(raw))
	}

	var tok struct {
		AccessToken string `json:"access_token"`
		IDToken     string `json:"id_token"`
		ExpiresIn   int    `json:"expires_in"`
		TokenType   string `json:"token_type"`
	}
	if err := json.Unmarshal(raw, &tok); err != nil {
		return nil, fmt.Errorf("zitadel: decode token response: %w", err)
	}
	if tok.AccessToken == "" {
		return nil, fmt.Errorf("zitadel: empty access_token")
	}
	return &TokenResult{
		AccessToken: tok.AccessToken,
		IDToken:     tok.IDToken,
		ExpiresIn:   tok.ExpiresIn,
		TokenType:   tok.TokenType,
	}, nil
}

// CreateHumanUserRequest creates a human user with email/password profile.
type CreateHumanUserRequest struct {
	OrgID         string
	Email         string
	Password      string
	DisplayName   string
	Language      string
	EmailVerified bool
}

// CreateHumanUserResult is the created ZITADEL user id (sub).
type CreateHumanUserResult struct {
	UserID string
}

// CreateHumanUser registers a human user via POST /v2/users/human.
func (c *Client) CreateHumanUser(ctx context.Context, req CreateHumanUserRequest) (*CreateHumanUserResult, error) {
	if c == nil {
		return nil, ErrNotConfigured
	}
	if c.conf.ServiceUserToken == "" {
		return nil, ErrNotConfigured
	}
	orgID := req.OrgID
	if orgID == "" {
		orgID = c.conf.DefaultOrgID
	}
	given, family := splitDisplayName(req.DisplayName, req.Email)
	profile := map[string]any{
		"givenName":  given,
		"familyName": family,
	}
	if req.DisplayName != "" {
		profile["displayName"] = req.DisplayName
	}
	if req.Language != "" {
		profile["preferredLanguage"] = req.Language
	}
	body := map[string]any{
		"username": req.Email,
		"profile":  profile,
		"email": map[string]any{
			"email":      req.Email,
			"isVerified": req.EmailVerified,
		},
		fieldPassword: map[string]any{
			fieldPassword:    req.Password,
			"changeRequired": false,
		},
	}
	if orgID != "" {
		body["organizationId"] = orgID
	}

	var out struct {
		UserID string `json:"userId"`
	}
	if err := c.doJSON(ctx, http.MethodPost, c.apiBase+"/v2/users/human", c.serviceAuth(), body, http.StatusOK, &out); err != nil {
		return nil, err
	}
	if out.UserID == "" {
		return nil, fmt.Errorf("zitadel: create user: empty userId in response")
	}
	return &CreateHumanUserResult{UserID: out.UserID}, nil
}

// SetUserPassword sets a human user's password via management API (PAT).
// When currentPassword is non-empty, ZITADEL validates the old password first.
func (c *Client) SetUserPassword(ctx context.Context, userID, newPassword, currentPassword string) error {
	if c == nil {
		return ErrNotConfigured
	}
	if userID == "" || newPassword == "" {
		return fmt.Errorf("zitadel: user id and new password are required")
	}
	body := map[string]any{
		"newPassword": map[string]any{
			fieldPassword:    newPassword,
			"changeRequired": false,
		},
	}
	if strings.TrimSpace(currentPassword) != "" {
		body["currentPassword"] = currentPassword
	}
	endpoint := c.apiBase + "/v2/users/" + url.PathEscape(userID) + "/password"
	return c.doJSON(ctx, http.MethodPost, endpoint, c.serviceAuth(), body, http.StatusOK, nil)
}

// DeactivateUser disables login for the user via POST /v2/users/{id}/deactivate.
func (c *Client) DeactivateUser(ctx context.Context, userID string) error {
	if c == nil {
		return ErrNotConfigured
	}
	if userID == "" {
		return fmt.Errorf("zitadel: user id is required")
	}
	return c.doJSON(ctx, http.MethodPost, c.apiBase+"/v2/users/"+url.PathEscape(userID)+"/deactivate", c.serviceAuth(), map[string]any{}, http.StatusOK, nil)
}

// TokenResult holds OAuth tokens from a successful password grant, or session-verified
// identity fields when OAuth ROPG is not configured (ZITADEL v2 default).
type TokenResult struct {
	AccessToken string
	IDToken     string
	ExpiresIn   int
	TokenType   string
	// Subject and Email are set by session-based verification (no OAuth tokens).
	Subject string
	Email   string
}

// VerifyPassword checks email/password credentials. ZITADEL v2 disables the
// resource-owner password grant by default, so when a service PAT is configured
// we use the Sessions API first. ROPG remains a fallback for legacy instances.
func (c *Client) VerifyPassword(ctx context.Context, username, password string) (*TokenResult, error) {
	if c == nil {
		return nil, ErrNotConfigured
	}
	if c.conf.ServiceUserToken != "" {
		return c.verifyPasswordSession(ctx, username, password)
	}
	if c.conf.OAuthClientID != "" && c.conf.OAuthClientSecret != "" {
		return c.verifyPasswordROPG(ctx, username, password)
	}
	return nil, fmt.Errorf("zitadel: password verification not configured (need service token or oauth client)")
}

func (c *Client) verifyPasswordROPG(ctx context.Context, username, password string) (*TokenResult, error) {
	form := url.Values{}
	form.Set("grant_type", fieldPassword)
	form.Set("client_id", c.conf.OAuthClientID)
	form.Set("client_secret", c.conf.OAuthClientSecret)
	form.Set("username", username)
	form.Set(fieldPassword, password)
	form.Set("scope", "openid profile email")

	return c.postToken(ctx, form)
}

func (c *Client) serviceAuth() string {
	return "Bearer " + c.conf.ServiceUserToken
}

func (c *Client) doJSON(ctx context.Context, method, endpoint, auth string, body any, wantStatus int, out any) error {
	var r io.Reader
	if body != nil {
		raw, err := json.Marshal(body)
		if err != nil {
			return fmt.Errorf("zitadel: marshal request: %w", err)
		}
		r = bytes.NewReader(raw)
	}
	req, err := http.NewRequestWithContext(ctx, method, endpoint, r)
	if err != nil {
		return fmt.Errorf("zitadel: new request: %w", err)
	}
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	req.Header.Set("Accept", "application/json")
	if auth != "" {
		req.Header.Set("Authorization", auth)
	}

	resp, err := c.http.Do(req)
	if err != nil {
		return fmt.Errorf("zitadel: %s %s: %w", method, endpoint, err)
	}
	defer resp.Body.Close()

	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("zitadel: read response body: %w", err)
	}
	if resp.StatusCode == http.StatusConflict {
		return ErrUserAlreadyExists
	}
	// Accept any 2xx as success. ZITADEL v2 returns 201 for create endpoints
	// (e.g. POST /v2/users/human) and 200 for most others; wantStatus is kept
	// for caller intent but we don't reject other 2xx responses.
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		_ = wantStatus
		return fmt.Errorf("zitadel: %s %s: status %d: %s", method, endpoint, resp.StatusCode, truncateBody(raw))
	}
	if out != nil && len(raw) > 0 {
		if err := json.Unmarshal(raw, out); err != nil {
			return fmt.Errorf("zitadel: decode response: %w", err)
		}
	}
	return nil
}

func splitDisplayName(displayName, email string) (given, family string) {
	displayName = strings.TrimSpace(displayName)
	if displayName == "" {
		local := email
		if i := strings.Index(email, "@"); i > 0 {
			local = email[:i]
		}
		return local, "-"
	}
	parts := strings.Fields(displayName)
	if len(parts) == 1 {
		return parts[0], "-"
	}
	return parts[0], strings.Join(parts[1:], " ")
}

func truncateBody(b []byte) string {
	const maxBodyLen = 512
	s := strings.TrimSpace(string(b))
	if len(s) > maxBodyLen {
		return s[:maxBodyLen] + "..."
	}
	return s
}
