package stalwart

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

var (
	ErrDisabled = errors.New("stalwart client disabled")
	ErrNotFound = errors.New("stalwart resource not found")
)

type Config struct {
	Enabled    bool
	BaseURL    string
	APIKey     string
	IMAPHost   string
	IMAPPort   int
	IMAPTLS    bool
	SMTPHost   string
	SMTPPort   int
	SMTPTLS    bool
	HTTPClient *http.Client
}

type Client struct {
	cfg Config
}

func NewClient(cfg Config) *Client {
	if cfg.HTTPClient == nil {
		cfg.HTTPClient = &http.Client{Timeout: 30 * time.Second}
	}
	if cfg.IMAPPort == 0 {
		cfg.IMAPPort = 993
	}
	if cfg.SMTPPort == 0 {
		cfg.SMTPPort = 587
	}
	return &Client{cfg: cfg}
}

func (c *Client) Enabled() bool {
	return c != nil && c.cfg.Enabled && strings.TrimSpace(c.cfg.BaseURL) != ""
}

func (c *Client) IMAPEndpoint() (host string, port int, tls bool) {
	return c.cfg.IMAPHost, c.cfg.IMAPPort, c.cfg.IMAPTLS
}

func (c *Client) SMTPEndpoint() (host string, port int, tls bool) {
	return c.cfg.SMTPHost, c.cfg.SMTPPort, c.cfg.SMTPTLS
}

type Domain struct {
	ID   string
	Name string
}

type Account struct {
	ID    string
	Email string
}

type jmapRequest struct {
	Using       []string        `json:"using"`
	MethodCalls []any           `json:"methodCalls"`
}

type jmapResponse struct {
	MethodResponses []json.RawMessage `json:"methodResponses"`
}

func (c *Client) call(ctx context.Context, method string, args any, callID string) (json.RawMessage, error) {
	if !c.Enabled() {
		return nil, ErrDisabled
	}
	body, err := json.Marshal(jmapRequest{
		Using: []string{"urn:ietf:params:jmap:core", "urn:stalwart:jmap"},
		MethodCalls: []any{[]any{method, args, callID}},
	})
	if err != nil {
		return nil, err
	}
	url := strings.TrimRight(c.cfg.BaseURL, "/") + "/api"
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")
	if c.cfg.APIKey != "" {
		req.Header.Set("Authorization", "Bearer "+c.cfg.APIKey)
	}
	resp, err := c.cfg.HTTPClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode >= 400 {
		return nil, fmt.Errorf("stalwart api %s: %s", resp.Status, strings.TrimSpace(string(raw)))
	}
	var envelope jmapResponse
	if err := json.Unmarshal(raw, &envelope); err != nil {
		return nil, err
	}
	if len(envelope.MethodResponses) == 0 {
		return nil, fmt.Errorf("stalwart: empty method response")
	}
	var parts []json.RawMessage
	if err := json.Unmarshal(envelope.MethodResponses[0], &parts); err != nil {
		return nil, err
	}
	if len(parts) < 2 {
		return nil, fmt.Errorf("stalwart: malformed method response")
	}
	var status string
	if err := json.Unmarshal(parts[0], &status); err != nil {
		return nil, err
	}
	if status == "error" {
		return nil, fmt.Errorf("stalwart error: %s", string(parts[1]))
	}
	return parts[1], nil
}

func (c *Client) CreateDomain(ctx context.Context, name string) (Domain, error) {
	name = strings.ToLower(strings.TrimSpace(name))
	if name == "" {
		return Domain{}, fmt.Errorf("domain name required")
	}
	if !c.Enabled() {
		return Domain{ID: "local-" + name, Name: name}, nil
	}
	raw, err := c.call(ctx, "x:Domain/set", map[string]any{
		"create": map[string]any{
			"d1": map[string]any{"name": name},
		},
	}, "c1")
	if err != nil {
		return Domain{}, err
	}
	var parsed struct {
		Created map[string]struct {
			ID string `json:"id"`
		} `json:"created"`
	}
	if err := json.Unmarshal(raw, &parsed); err != nil {
		return Domain{}, err
	}
	for _, v := range parsed.Created {
		return Domain{ID: v.ID, Name: name}, nil
	}
	return Domain{}, fmt.Errorf("stalwart: domain not created")
}

func (c *Client) CreateAccount(ctx context.Context, domainID, localPart, password string, quotaBytes int64) (Account, error) {
	localPart = strings.ToLower(strings.TrimSpace(localPart))
	if localPart == "" {
		return Account{}, fmt.Errorf("local part required")
	}
	if !c.Enabled() {
		return Account{ID: "local-" + localPart, Email: localPart + "@local"}, nil
	}
	fields := map[string]any{
		"name":     localPart,
		"domainId": domainID,
	}
	if password != "" {
		fields["credentials"] = map[string]any{"password": password}
	}
	if quotaBytes > 0 {
		fields["quota"] = map[string]any{"maxDiskQuota": quotaBytes}
	}
	raw, err := c.call(ctx, "x:Account/set", map[string]any{
		"create": map[string]any{"a1": fields},
	}, "c1")
	if err != nil {
		return Account{}, err
	}
	var parsed struct {
		Created map[string]struct {
			ID string `json:"id"`
		} `json:"created"`
	}
	if err := json.Unmarshal(raw, &parsed); err != nil {
		return Account{}, err
	}
	for _, v := range parsed.Created {
		return Account{ID: v.ID, Email: localPart}, nil
	}
	return Account{}, fmt.Errorf("stalwart: account not created")
}

func (c *Client) SetAccountPassword(ctx context.Context, accountID, password string) error {
	if password == "" {
		return fmt.Errorf("password required")
	}
	if !c.Enabled() {
		return nil
	}
	_, err := c.call(ctx, "x:Account/set", map[string]any{
		"update": map[string]any{
			accountID: map[string]any{
				"credentials": map[string]any{"password": password},
			},
		},
	}, "c1")
	return err
}

func (c *Client) DeleteAccount(ctx context.Context, accountID string) error {
	if accountID == "" {
		return nil
	}
	if !c.Enabled() {
		return nil
	}
	_, err := c.call(ctx, "x:Account/set", map[string]any{
		"destroy": []string{accountID},
	}, "c1")
	return err
}
