// Package notify provides a notify service admin client for rdev.
// It manages accounts and send keys on behalf of projects.
package notify

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

// notifyAdminAPI is the interface Provisioner uses to call the notify admin API.
// Extracted for testability.
type notifyAdminAPI interface {
	createHost(ctx context.Context, hostSlug, strategy string) error
	deleteHost(ctx context.Context, hostSlug string) error
	createProvider(ctx context.Context, hostSlug, provider string, config map[string]string, priority, retryAttempts, retryBackoffMs int) error
	createFromAddress(ctx context.Context, hostSlug, email, displayName string) error
	createAccount(ctx context.Context, name string) (*accountResponse, error)
	createSendKey(ctx context.Context, accountID, name string) (*apiKeyResponse, error)
	grantHostAccess(ctx context.Context, hostSlug, accountID string) error
	deleteAccount(ctx context.Context, accountID string) error
	listAccounts(ctx context.Context) ([]accountResponse, error)
}

// adminClient calls the notify admin API to manage accounts and keys.
type adminClient struct {
	baseURL    string
	adminKey   string
	httpClient *http.Client
}

func newAdminClient(baseURL, adminKey string) *adminClient {
	return &adminClient{
		baseURL:  baseURL,
		adminKey: adminKey,
		httpClient: &http.Client{
			Timeout: 30 * time.Second,
		},
	}
}

// accountResponse is the shape returned by POST /admin/accounts.
type accountResponse struct {
	ID        string    `json:"id"`
	Name      string    `json:"name"`
	CreatedAt time.Time `json:"created_at"`
}

// apiKeyResponse is the shape returned by POST /admin/api-keys (full key only on creation).
type apiKeyResponse struct {
	ID        int       `json:"id"`
	Key       string    `json:"key"`        // plaintext — only present on creation
	KeyPrefix string    `json:"key_prefix"` // e.g. "notify_send"
	AccountID string    `json:"account_id"`
	KeyType   string    `json:"key_type"`
	Name      string    `json:"name"`
	CreatedAt time.Time `json:"created_at"`
}

// listAccountsResponse is the shape returned by GET /admin/accounts.
type listAccountsResponse struct {
	Items []accountResponse `json:"items"`
}

// createAccount creates a new notify account with the given name.
func (c *adminClient) createAccount(ctx context.Context, name string) (*accountResponse, error) {
	payload := map[string]string{"name": name}
	respBody, err := c.doRequest(ctx, http.MethodPost, "/admin/accounts", payload)
	if err != nil {
		return nil, fmt.Errorf("create account: %w", err)
	}

	var acct accountResponse
	if err := json.Unmarshal(respBody, &acct); err != nil {
		return nil, fmt.Errorf("unmarshal account response: %w", err)
	}
	return &acct, nil
}

// createSendKey creates a send API key for the given account.
// The plaintext key is only present in the response at creation time.
func (c *adminClient) createSendKey(ctx context.Context, accountID, name string) (*apiKeyResponse, error) {
	payload := map[string]string{
		"account_id": accountID,
		"key_type":   "send",
		"name":       name,
	}
	respBody, err := c.doRequest(ctx, http.MethodPost, "/admin/api-keys", payload)
	if err != nil {
		return nil, fmt.Errorf("create send key: %w", err)
	}

	var key apiKeyResponse
	if err := json.Unmarshal(respBody, &key); err != nil {
		return nil, fmt.Errorf("unmarshal key response: %w", err)
	}
	return &key, nil
}

// createHost creates a new notify sending host with the given slug and sending strategy.
func (c *adminClient) createHost(ctx context.Context, hostSlug, strategy string) error {
	payload := map[string]string{"host": hostSlug, "strategy": strategy}
	_, err := c.doRequest(ctx, http.MethodPost, "/admin/hosts", payload)
	if err != nil {
		return fmt.Errorf("create host %s: %w", hostSlug, err)
	}
	return nil
}

// deleteHost removes a notify host by its slug.
func (c *adminClient) deleteHost(ctx context.Context, hostSlug string) error {
	_, err := c.doRequest(ctx, http.MethodDelete, "/admin/hosts/"+hostSlug, nil)
	if err != nil {
		return fmt.Errorf("delete host %s: %w", hostSlug, err)
	}
	return nil
}

// createProvider adds a sending provider to an existing host.
func (c *adminClient) createProvider(ctx context.Context, hostSlug, provider string, config map[string]string, priority, retryAttempts, retryBackoffMs int) error {
	payload := map[string]any{
		"provider":         provider,
		"config":           config,
		"priority":         priority,
		"retry_attempts":   retryAttempts,
		"retry_backoff_ms": retryBackoffMs,
	}
	_, err := c.doRequest(ctx, http.MethodPost, "/admin/hosts/"+hostSlug+"/providers", payload)
	if err != nil {
		return fmt.Errorf("create provider %s on host %s: %w", provider, hostSlug, err)
	}
	return nil
}

// createFromAddress registers a from-address on a host.
func (c *adminClient) createFromAddress(ctx context.Context, hostSlug, email, displayName string) error {
	payload := map[string]string{"email": email, "display_name": displayName}
	_, err := c.doRequest(ctx, http.MethodPost, "/admin/hosts/"+hostSlug+"/from-addresses", payload)
	if err != nil {
		return fmt.Errorf("create from-address %s on host %s: %w", email, hostSlug, err)
	}
	return nil
}

// grantHostAccess grants the given account access to send from the specified host slug.
func (c *adminClient) grantHostAccess(ctx context.Context, hostSlug, accountID string) error {
	payload := map[string]string{"account_id": accountID}
	_, err := c.doRequest(ctx, http.MethodPost, "/admin/hosts/"+hostSlug+"/accounts", payload)
	if err != nil {
		return fmt.Errorf("grant host access: %w", err)
	}
	return nil
}

// deleteAccount removes the notify account and all its keys.
func (c *adminClient) deleteAccount(ctx context.Context, accountID string) error {
	_, err := c.doRequest(ctx, http.MethodDelete, "/admin/accounts/"+accountID, nil)
	if err != nil {
		return fmt.Errorf("delete account: %w", err)
	}
	return nil
}

// listAccounts returns all accounts in the notify service.
func (c *adminClient) listAccounts(ctx context.Context) ([]accountResponse, error) {
	respBody, err := c.doRequest(ctx, http.MethodGet, "/admin/accounts", nil)
	if err != nil {
		return nil, fmt.Errorf("list accounts: %w", err)
	}

	var resp listAccountsResponse
	if err := json.Unmarshal(respBody, &resp); err != nil {
		return nil, fmt.Errorf("unmarshal accounts list: %w", err)
	}
	return resp.Items, nil
}

// doRequest executes an HTTP request against the notify admin API.
func (c *adminClient) doRequest(ctx context.Context, method, path string, bodyData any) ([]byte, error) {
	var reqBody io.Reader
	if bodyData != nil {
		jsonBody, err := json.Marshal(bodyData)
		if err != nil {
			return nil, fmt.Errorf("marshal request: %w", err)
		}
		reqBody = bytes.NewReader(jsonBody)
	}

	req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, reqBody)
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+c.adminKey)
	if bodyData != nil {
		req.Header.Set("Content-Type", "application/json")
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("http do: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	// 204 No Content — success with no body (e.g., grant host access, delete)
	if resp.StatusCode == http.StatusNoContent {
		return nil, nil
	}

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("read response body: %w", err)
	}

	if resp.StatusCode >= 200 && resp.StatusCode < 300 {
		return respBody, nil
	}

	return nil, fmt.Errorf("notify admin API error (HTTP %d): %s", resp.StatusCode, string(respBody))
}
