package nextcloud

import (
	"context"
	"crypto/rand"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"math/big"
	"net/http"
	"net/url"
	"strings"
)

var ErrPrincipalNotFound = errors.New("nextcloud principal not found")

// UserIDFromClaims returns the Nextcloud account id aligned with user_oidc mapping-uid
// (preferred_username / enrollment email), not the opaque OIDC sub.
func UserIDFromClaims(email, sub string) string {
	email = strings.TrimSpace(strings.ToLower(email))
	if email != "" {
		return email
	}
	return strings.TrimSpace(sub)
}

// EnsurePrincipal provisions a Nextcloud user and CardDAV app credentials.
func (c *Client) EnsurePrincipal(ctx context.Context, email, sub, displayName string) (string, error) {
	if c.credStore == nil {
		return "", fmt.Errorf("nextcloud dav credentials store not configured")
	}
	userID := UserIDFromClaims(email, sub)
	if userID == "" {
		return "", fmt.Errorf("nextcloud user id is empty")
	}

	token, err := c.credStore.GetToken(ctx, userID)
	if err == nil && token != "" {
		return userID, nil
	}

	exists, err := c.userExists(ctx, userID)
	if err != nil {
		return "", err
	}

	provisionEmail := strings.TrimSpace(email)
	if provisionEmail == "" {
		provisionEmail = userID
	}
	name := strings.TrimSpace(displayName)
	if name == "" {
		name = provisionEmail
	}

	loginPassword, err := generateNextcloudPassword()
	if err != nil {
		return "", err
	}

	if !exists {
		if err := c.createUser(ctx, userID, provisionEmail, name, loginPassword); err != nil {
			return "", err
		}
	} else if err := c.setUserPassword(ctx, userID, loginPassword); err != nil {
		return "", err
	}

	appPassword, err := c.createAppPassword(ctx, userID, loginPassword)
	if err != nil {
		return "", err
	}
	if err := c.credStore.SaveToken(ctx, userID, appPassword); err != nil {
		return "", err
	}
	return userID, nil
}

// InvalidatePrincipalCredentials removes stored CardDAV app credentials for a user.
func (c *Client) InvalidatePrincipalCredentials(ctx context.Context, userID string) error {
	if c.credStore == nil {
		return nil
	}
	return c.credStore.DeleteToken(ctx, userID)
}

// RefreshPrincipalCredentials rotates the Nextcloud login password and app password for an existing user.
func (c *Client) RefreshPrincipalCredentials(ctx context.Context, userID string) error {
	if c.credStore == nil {
		return fmt.Errorf("nextcloud dav credentials store not configured")
	}
	userID = strings.TrimSpace(userID)
	if userID == "" {
		return fmt.Errorf("nextcloud user id is empty")
	}

	loginPassword, err := generateNextcloudPassword()
	if err != nil {
		return err
	}
	if err := c.setUserPassword(ctx, userID, loginPassword); err != nil {
		return err
	}
	appPassword, err := c.createAppPassword(ctx, userID, loginPassword)
	if err != nil {
		return err
	}
	return c.credStore.SaveToken(ctx, userID, appPassword)
}

// UserExists reports whether a Nextcloud account exists for the given user id (typically email).
func (c *Client) UserExists(ctx context.Context, userID string) (bool, error) {
	return c.userExists(ctx, userID)
}

// UserDisplayName returns the Nextcloud account display name for a user id.
func (c *Client) UserDisplayName(ctx context.Context, userID string) (string, error) {
	userID = strings.TrimSpace(userID)
	if userID == "" {
		return "", fmt.Errorf("empty user id")
	}
	path := fmt.Sprintf("/ocs/v1.php/cloud/users/%s?format=json", url.PathEscape(userID))
	resp, err := c.doRequest(ctx, http.MethodGet, path, nil, map[string]string{
		"Accept": "application/json",
	})
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return "", &HTTPStatusError{Operation: "get user display name", StatusCode: resp.StatusCode}
	}
	var payload struct {
		OCS struct {
			Data struct {
				DisplayName string `json:"displayname"`
				ID          string `json:"id"`
			} `json:"data"`
		} `json:"ocs"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
		return "", err
	}
	name := strings.TrimSpace(payload.OCS.Data.DisplayName)
	if name != "" {
		return name, nil
	}
	return strings.TrimSpace(payload.OCS.Data.ID), nil
}

func (c *Client) userExists(ctx context.Context, userID string) (bool, error) {
	path := fmt.Sprintf("/ocs/v1.php/cloud/users/%s?format=json", url.PathEscape(userID))
	resp, err := c.doRequest(ctx, http.MethodGet, path, nil, map[string]string{
		"OCS-APIRequest": "true",
		"Accept":         "application/json",
	})
	if err != nil {
		return false, err
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusNotFound {
		return false, nil
	}
	if resp.StatusCode != http.StatusOK {
		return false, &HTTPStatusError{Operation: "get user", StatusCode: resp.StatusCode}
	}

	var payload struct {
		OCS struct {
			Meta struct {
				StatusCode int `json:"statuscode"`
			} `json:"meta"`
		} `json:"ocs"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
		return false, err
	}
	return payload.OCS.Meta.StatusCode == 100, nil
}

func (c *Client) setUserPassword(ctx context.Context, userID, password string) error {
	form := url.Values{}
	form.Set("key", "password")
	form.Set("value", password)
	path := fmt.Sprintf("/ocs/v1.php/cloud/users/%s", url.PathEscape(userID))
	resp, err := c.doRequest(ctx, http.MethodPut, path, strings.NewReader(form.Encode()), map[string]string{
		"OCS-APIRequest": "true",
		"Content-Type":   "application/x-www-form-urlencoded",
		"Accept":         "application/json",
	})
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return &HTTPStatusError{Operation: "set user password", StatusCode: resp.StatusCode}
	}
	var payload struct {
		OCS struct {
			Meta struct {
				Status     string `json:"status"`
				StatusCode int    `json:"statuscode"`
			} `json:"meta"`
		} `json:"ocs"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
		return err
	}
	if strings.EqualFold(payload.OCS.Meta.Status, "ok") || payload.OCS.Meta.StatusCode == 100 {
		return nil
	}
	return fmt.Errorf("set nextcloud user password failed with status %d", payload.OCS.Meta.StatusCode)
}

func (c *Client) createAppPassword(ctx context.Context, userID, loginPassword string) (string, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/ocs/v2.php/core/getapppassword?format=json", nil)
	if err != nil {
		return "", err
	}
	req.SetBasicAuth(userID, loginPassword)
	req.Header.Set("OCS-APIRequest", "true")
	req.Header.Set("Accept", "application/json")

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return "", &HTTPStatusError{Operation: "create app password", StatusCode: resp.StatusCode}
	}
	var payload struct {
		OCS struct {
			Data struct {
				AppPassword string `json:"apppassword"`
			} `json:"data"`
		} `json:"ocs"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
		return "", err
	}
	token := strings.TrimSpace(payload.OCS.Data.AppPassword)
	if token == "" {
		return "", fmt.Errorf("nextcloud app password response empty")
	}
	return token, nil
}

func (c *Client) createUser(ctx context.Context, userID, email, displayName, password string) error {
	form := url.Values{}
	form.Set("userid", userID)
	form.Set("password", password)
	form.Set("email", email)
	form.Set("displayName", displayName)

	resp, err := c.doRequest(ctx, http.MethodPost, "/ocs/v1.php/cloud/users?format=json", strings.NewReader(form.Encode()), map[string]string{
		"OCS-APIRequest": "true",
		"Content-Type":   "application/x-www-form-urlencoded",
		"Accept":         "application/json",
	})
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return &HTTPStatusError{Operation: "create user", StatusCode: resp.StatusCode}
	}

	var payload struct {
		OCS struct {
			Meta struct {
				Status     string `json:"status"`
				StatusCode int    `json:"statuscode"`
				Message    string `json:"message"`
			} `json:"meta"`
		} `json:"ocs"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
		return err
	}
	if strings.EqualFold(payload.OCS.Meta.Status, "ok") || payload.OCS.Meta.StatusCode == 100 {
		return nil
	}
	if payload.OCS.Meta.Message != "" {
		return fmt.Errorf("create nextcloud user: %s", payload.OCS.Meta.Message)
	}
	return fmt.Errorf("create nextcloud user failed with status %d", payload.OCS.Meta.StatusCode)
}

func generateNextcloudPassword() (string, error) {
	const (
		lower   = "abcdefghijklmnopqrstuvwxyz"
		upper   = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
		digits  = "0123456789"
		symbols = "!@#$%^&*()-_=+"
		all     = lower + upper + digits + symbols
	)
	pick := func(chars string) (byte, error) {
		n, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
		if err != nil {
			return 0, err
		}
		return chars[n.Int64()], nil
	}

	out := make([]byte, 32)
	required := []string{lower, upper, digits, symbols}
	for i, chars := range required {
		b, err := pick(chars)
		if err != nil {
			return "", err
		}
		out[i] = b
	}
	for i := len(required); i < len(out); i++ {
		b, err := pick(all)
		if err != nil {
			return "", err
		}
		out[i] = b
	}
	for i := len(out) - 1; i > 0; i-- {
		j, err := rand.Int(rand.Reader, big.NewInt(int64(i+1)))
		if err != nil {
			return "", err
		}
		out[i], out[j.Int64()] = out[j.Int64()], out[i]
	}
	return string(out), nil
}

func readResponseBody(resp *http.Response) ([]byte, error) {
	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	return raw, nil
}

func davResponseError(raw []byte, statusCode int) error {
	if statusCode == http.StatusNotFound {
		return ErrPrincipalNotFound
	}
	if statusCode != http.StatusMultiStatus && statusCode != http.StatusOK {
		if strings.Contains(string(raw), "<d:error") {
			return &HTTPStatusError{Operation: "carddav", StatusCode: statusCode}
		}
		return &HTTPStatusError{Operation: "carddav", StatusCode: statusCode}
	}
	return nil
}

func addressBookHomePath(userID string) string {
	return fmt.Sprintf("/remote.php/dav/addressbooks/users/%s/", url.PathEscape(userID))
}

// AddressBookPath returns the CardDAV collection path for a user's address book.
func AddressBookPath(userID, bookID string) string {
	return addressBookHomePath(userID) + url.PathEscape(bookID) + "/"
}
