package v1

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

	"github.com/superfly/flyctl/internal/config"
)

func (c *Client) ListManagedClusters(ctx context.Context, orgSlug string, deleted bool) (ListManagedClustersResponse, error) {
	var response ListManagedClustersResponse

	cfg := config.FromContext(ctx)
	url := fmt.Sprintf("%s/api/v1/organizations/%s/postgres", c.BaseURL(), orgSlug)
	if deleted {
		url = fmt.Sprintf("%s/api/v1/organizations/%s/postgres/deleted", c.BaseURL(), orgSlug)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return response, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Add("Authorization", "Bearer "+cfg.Tokens.GraphQL())
	req.Header.Add("Content-Type", "application/json")

	res, err := c.HTTPClient().Do(req)
	if err != nil {
		return response, err
	}
	defer res.Body.Close()

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

	switch res.StatusCode {
	case http.StatusOK:
		if err = json.Unmarshal(body, &response); err != nil {
			return response, fmt.Errorf("failed to decode response, please try again: %w", err)
		}

		return response, nil
	case http.StatusNotFound:
		return response, fmt.Errorf("organization %s not found", orgSlug)
	default:
		return response, fmt.Errorf("failed to list clusters (status %d): %s", res.StatusCode, string(body))
	}
}

func (c *Client) GetManagedCluster(ctx context.Context, orgSlug string, id string) (GetManagedClusterResponse, error) {
	var response GetManagedClusterResponse
	cfg := config.FromContext(ctx)
	url := fmt.Sprintf("%s/api/v1/organizations/%s/postgres/%s", c.BaseURL(), orgSlug, id)

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return response, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Add("Authorization", "Bearer "+cfg.Tokens.GraphQL())
	req.Header.Add("Content-Type", "application/json")

	res, err := c.HTTPClient().Do(req)
	if err != nil {
		return response, err
	}
	defer res.Body.Close()

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

	switch res.StatusCode {
	case http.StatusOK:
		if err = json.Unmarshal(body, &response); err != nil {
			return response, fmt.Errorf("failed to decode response, please try again: %w", err)
		}

		return response, nil
	case http.StatusNotFound:
		return response, fmt.Errorf("cluster %s not found in organization %s", id, orgSlug)
	default:
		return response, fmt.Errorf("failed to get cluster (status %d): %s", res.StatusCode, string(body))
	}
}

func (c *Client) GetManagedClusterById(ctx context.Context, id string) (GetManagedClusterResponse, error) {
	var response GetManagedClusterResponse
	cfg := config.FromContext(ctx)
	url := fmt.Sprintf("%s/api/v1/postgres/%s", c.BaseURL(), id)

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return response, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Add("Authorization", "Bearer "+cfg.Tokens.GraphQL())
	req.Header.Add("Content-Type", "application/json")

	res, err := c.HTTPClient().Do(req)
	if err != nil {
		return response, err
	}
	defer res.Body.Close()

	switch res.StatusCode {
	case http.StatusOK:
		if err = json.NewDecoder(res.Body).Decode(&response); err != nil {
			return response, fmt.Errorf("failed to decode response, please try again: %w", err)
		}

		return response, nil
	case http.StatusNotFound:
		return response, fmt.Errorf("Cluster %s not found", id)
	default:
		return response, fmt.Errorf("Something went wrong")
	}
}
func (c *Client) CreateUser(ctx context.Context, id string, input CreateUserInput) (CreateUserResponse, error) {
	var response CreateUserResponse
	cfg := config.FromContext(ctx)
	url := fmt.Sprintf("%s/api/v1/postgres/%s/users", c.BaseURL(), id)

	var buf bytes.Buffer
	if err := json.NewEncoder(&buf).Encode(input); err != nil {
		return response, err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf)
	if err != nil {
		return response, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Add("Authorization", "Bearer "+cfg.Tokens.GraphQL())
	req.Header.Add("Content-Type", "application/json")

	res, err := c.HTTPClient().Do(req)
	if err != nil {
		return response, err
	}
	defer res.Body.Close()

	switch res.StatusCode {
	case http.StatusCreated:
		if err = json.NewDecoder(res.Body).Decode(&response); err != nil {
			return response, fmt.Errorf("failed to decode response, please try again: %w", err)
		}

		if !response.Ok {
			if response.Errors.Detail != "" {
				return response, fmt.Errorf("Failed to create user with error: %s", response.Errors.Detail)
			} else {
				return response, fmt.Errorf("Something went wrong creating user. Please try again")
			}
		}

		return response, nil

	default:
		if err = json.NewDecoder(res.Body).Decode(&response); err != nil {
			return response, fmt.Errorf("failed to decode response, please try again: %w", err)
		}

		if response.Errors.Detail != "" {
			return response, fmt.Errorf("Failed to create user with error: %s", response.Errors.Detail)
		}

		return response, fmt.Errorf("Failed to create user with error: %s", response.Errors.Detail)
	}
}

func (c *Client) CreateUserWithRole(ctx context.Context, id string, input CreateUserWithRoleInput) (CreateUserWithRoleResponse, error) {
	var response CreateUserWithRoleResponse
	cfg := config.FromContext(ctx)
	url := fmt.Sprintf("%s/api/v1/postgres/%s/users", c.BaseURL(), id)

	var buf bytes.Buffer
	if err := json.NewEncoder(&buf).Encode(input); err != nil {
		return response, fmt.Errorf("failed to encode request body: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf)
	if err != nil {
		return response, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Add("Authorization", "Bearer "+cfg.Tokens.GraphQL())
	req.Header.Add("Content-Type", "application/json")

	res, err := c.HTTPClient().Do(req)
	if err != nil {
		return response, err
	}
	defer res.Body.Close()

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

	switch res.StatusCode {
	case http.StatusOK, http.StatusCreated:
		if err = json.Unmarshal(body, &response); err != nil {
			return response, fmt.Errorf("failed to decode response: %w", err)
		}

		return response, nil
	case http.StatusNotFound:
		return response, fmt.Errorf("cluster %s not found", id)
	case http.StatusForbidden:
		return response, fmt.Errorf("access denied: you don't have permission to create users for cluster %s", id)
	default:
		return response, fmt.Errorf("failed to create user (status %d): %s", res.StatusCode, string(body))
	}
}

func (c *Client) UpdateUserRole(ctx context.Context, id string, username string, input UpdateUserRoleInput) (UpdateUserRoleResponse, error) {
	var response UpdateUserRoleResponse
	cfg := config.FromContext(ctx)
	url := fmt.Sprintf("%s/api/v1/postgres/%s/users/%s", c.BaseURL(), id, username)

	var buf bytes.Buffer
	if err := json.NewEncoder(&buf).Encode(input); err != nil {
		return response, fmt.Errorf("failed to encode request body: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPatch, url, &buf)
	if err != nil {
		return response, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Add("Authorization", "Bearer "+cfg.Tokens.GraphQL())
	req.Header.Add("Content-Type", "application/json")

	res, err := c.HTTPClient().Do(req)
	if err != nil {
		return response, err
	}
	defer res.Body.Close()

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

	switch res.StatusCode {
	case http.StatusOK:
		if err = json.Unmarshal(body, &response); err != nil {
			return response, fmt.Errorf("failed to decode response: %w", err)
		}

		return response, nil
	case http.StatusNotFound:
		return response, fmt.Errorf("cluster %s or user %s not found", id, username)
	case http.StatusForbidden:
		return response, fmt.Errorf("access denied: you don't have permission to update users for cluster %s", id)
	default:
		return response, fmt.Errorf("failed to update user role (status %d): %s", res.StatusCode, string(body))
	}
}

func (c *Client) DeleteUser(ctx context.Context, id string, username string) error {
	cfg := config.FromContext(ctx)
	url := fmt.Sprintf("%s/api/v1/postgres/%s/users/%s", c.BaseURL(), id, username)

	req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Add("Authorization", "Bearer "+cfg.Tokens.GraphQL())
	req.Header.Add("Content-Type", "application/json")

	res, err := c.HTTPClient().Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()

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

	switch res.StatusCode {
	case http.StatusOK, http.StatusNoContent:
		return nil
	case http.StatusNotFound:
		return fmt.Errorf("cluster %s or user %s not found", id, username)
	case http.StatusForbidden:
		return fmt.Errorf("access denied: you don't have permission to delete users for cluster %s", id)
	default:
		return fmt.Errorf("failed to delete user (status %d): %s", res.StatusCode, string(body))
	}
}

func (c *Client) GetUserCredentials(ctx context.Context, id string, username string) (GetUserCredentialsResponse, error) {
	var response GetUserCredentialsResponse
	cfg := config.FromContext(ctx)
	url := fmt.Sprintf("%s/api/v1/postgres/%s/users/%s/credentials", c.BaseURL(), id, username)

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return response, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Add("Authorization", "Bearer "+cfg.Tokens.GraphQL())
	req.Header.Add("Content-Type", "application/json")

	res, err := c.HTTPClient().Do(req)
	if err != nil {
		return response, err
	}
	defer res.Body.Close()

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

	switch res.StatusCode {
	case http.StatusOK:
		if err = json.Unmarshal(body, &response); err != nil {
			return response, fmt.Errorf("failed to decode response: %w", err)
		}

		return response, nil
	case http.StatusNotFound:
		return response, fmt.Errorf("cluster %s or user %s not found", id, username)
	case http.StatusForbidden:
		return response, fmt.Errorf("access denied: you don't have permission to get credentials for user %s in cluster %s", username, id)
	default:
		return response, fmt.Errorf("failed to get user credentials (status %d): %s", res.StatusCode, string(body))
	}
}

func (c *Client) ListUsers(ctx context.Context, id string) (ListUsersResponse, error) {
	var response ListUsersResponse
	cfg := config.FromContext(ctx)
	url := fmt.Sprintf("%s/api/v1/postgres/%s/users", c.BaseURL(), id)

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return response, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Add("Authorization", "Bearer "+cfg.Tokens.GraphQL())
	req.Header.Add("Content-Type", "application/json")

	res, err := c.HTTPClient().Do(req)
	if err != nil {
		return response, err
	}
	defer res.Body.Close()

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

	switch res.StatusCode {
	case http.StatusOK:
		if err = json.Unmarshal(body, &response); err != nil {
			return response, fmt.Errorf("failed to decode response: %w", err)
		}

		return response, nil
	case http.StatusNotFound:
		return response, fmt.Errorf("cluster %s not found", id)
	case http.StatusForbidden:
		return response, fmt.Errorf("access denied: you don't have permission to list users for cluster %s", id)
	default:
		return response, fmt.Errorf("failed to list users (status %d): %s", res.StatusCode, string(body))
	}
}

func (c *Client) ListDatabases(ctx context.Context, id string) (ListDatabasesResponse, error) {
	var response ListDatabasesResponse
	cfg := config.FromContext(ctx)
	url := fmt.Sprintf("%s/api/v1/postgres/%s/databases", c.BaseURL(), id)

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return response, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Add("Authorization", "Bearer "+cfg.Tokens.GraphQL())
	req.Header.Add("Content-Type", "application/json")

	res, err := c.HTTPClient().Do(req)
	if err != nil {
		return response, err
	}
	defer res.Body.Close()

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

	switch res.StatusCode {
	case http.StatusOK:
		if err = json.Unmarshal(body, &response); err != nil {
			return response, fmt.Errorf("failed to decode response: %w", err)
		}

		return response, nil
	case http.StatusNotFound:
		return response, fmt.Errorf("cluster %s not found", id)
	case http.StatusForbidden:
		return response, fmt.Errorf("access denied: you don't have permission to list databases for cluster %s", id)
	default:
		return response, fmt.Errorf("failed to list databases (status %d): %s", res.StatusCode, string(body))
	}
}

func (c *Client) CreateDatabase(ctx context.Context, id string, input CreateDatabaseInput) (CreateDatabaseResponse, error) {
	var response CreateDatabaseResponse
	cfg := config.FromContext(ctx)
	url := fmt.Sprintf("%s/api/v1/postgres/%s/databases", c.BaseURL(), id)

	var buf bytes.Buffer
	if err := json.NewEncoder(&buf).Encode(input); err != nil {
		return response, fmt.Errorf("failed to encode request body: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf)
	if err != nil {
		return response, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Add("Authorization", "Bearer "+cfg.Tokens.GraphQL())
	req.Header.Add("Content-Type", "application/json")

	res, err := c.HTTPClient().Do(req)
	if err != nil {
		return response, err
	}
	defer res.Body.Close()

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

	switch res.StatusCode {
	case http.StatusOK, http.StatusCreated:
		if err = json.Unmarshal(body, &response); err != nil {
			return response, fmt.Errorf("failed to decode response: %w", err)
		}

		return response, nil
	case http.StatusNotFound:
		return response, fmt.Errorf("cluster %s not found", id)
	case http.StatusForbidden:
		return response, fmt.Errorf("access denied: you don't have permission to create databases for cluster %s", id)
	default:
		return response, fmt.Errorf("failed to create database (status %d): %s", res.StatusCode, string(body))
	}
}

// ListManagedClusterBackups returns the list of backups for a managed Postgres cluster
func (c *Client) ListManagedClusterBackups(ctx context.Context, clusterID string) (ListManagedClusterBackupsResponse, error) {
	var response ListManagedClusterBackupsResponse
	cfg := config.FromContext(ctx)
	url := fmt.Sprintf("%s/api/v1/postgres/%s/backups", c.BaseURL(), clusterID)

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return response, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Add("Authorization", "Bearer "+cfg.Tokens.GraphQL())
	req.Header.Add("Content-Type", "application/json")

	res, err := c.HTTPClient().Do(req)
	if err != nil {
		return response, err
	}
	defer res.Body.Close()

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

	switch res.StatusCode {
	case http.StatusOK:
		if err = json.Unmarshal(body, &response); err != nil {
			return response, fmt.Errorf("failed to decode response: %w", err)
		}

		return response, nil
	case http.StatusNotFound:
		return response, fmt.Errorf("cluster %s not found", clusterID)
	case http.StatusForbidden:
		return response, fmt.Errorf("access denied: you don't have permission to list backups for cluster %s", clusterID)
	default:
		return response, fmt.Errorf("failed to list backups (status %d): %s", res.StatusCode, string(body))
	}
}

// CreateManagedClusterBackup creates a new backup for a managed Postgres cluster
func (c *Client) CreateManagedClusterBackup(ctx context.Context, clusterID string, input CreateManagedClusterBackupInput) (CreateManagedClusterBackupResponse, error) {
	var response CreateManagedClusterBackupResponse
	cfg := config.FromContext(ctx)
	url := fmt.Sprintf("%s/api/v1/postgres/%s/backups", c.BaseURL(), clusterID)

	var buf bytes.Buffer
	if err := json.NewEncoder(&buf).Encode(input); err != nil {
		return response, fmt.Errorf("failed to encode request body: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf)
	if err != nil {
		return response, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Add("Authorization", "Bearer "+cfg.Tokens.GraphQL())
	req.Header.Add("Content-Type", "application/json")

	res, err := c.HTTPClient().Do(req)
	if err != nil {
		return response, err
	}
	defer res.Body.Close()

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

	switch res.StatusCode {
	case http.StatusOK, http.StatusCreated:
		if err = json.Unmarshal(body, &response); err != nil {
			return response, fmt.Errorf("failed to decode response: %w", err)
		}

		return response, nil
	case http.StatusNotFound:
		return response, fmt.Errorf("cluster %s not found", clusterID)
	case http.StatusForbidden:
		return response, fmt.Errorf("access denied: you don't have permission to create backups for cluster %s", clusterID)
	default:
		return response, fmt.Errorf("failed to create backup (status %d): %s", res.StatusCode, string(body))
	}
}

// RestoreManagedClusterBackup restores a managed Postgres cluster from a backup
func (c *Client) RestoreManagedClusterBackup(ctx context.Context, clusterID string, input RestoreManagedClusterBackupInput) (RestoreManagedClusterBackupResponse, error) {
	var response RestoreManagedClusterBackupResponse
	cfg := config.FromContext(ctx)
	url := fmt.Sprintf("%s/api/v1/postgres/%s/restore", c.BaseURL(), clusterID)

	var buf bytes.Buffer
	if err := json.NewEncoder(&buf).Encode(input); err != nil {
		return response, fmt.Errorf("failed to encode request body: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf)
	if err != nil {
		return response, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Add("Authorization", "Bearer "+cfg.Tokens.GraphQL())
	req.Header.Add("Content-Type", "application/json")

	res, err := c.HTTPClient().Do(req)
	if err != nil {
		return response, err
	}
	defer res.Body.Close()

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

	switch res.StatusCode {
	case http.StatusOK, http.StatusCreated:
		if err = json.Unmarshal(body, &response); err != nil {
			return response, fmt.Errorf("failed to decode response: %w", err)
		}

		return response, nil
	case http.StatusNotFound:
		return response, fmt.Errorf("cluster %s not found", clusterID)
	case http.StatusForbidden:
		return response, fmt.Errorf("access denied: you don't have permission to restore cluster %s", clusterID)
	default:
		return response, fmt.Errorf("failed to restore cluster (status %d): %s", res.StatusCode, string(body))
	}
}

// DestroyCluster permanently destroys a managed Postgres cluster
func (c *Client) DestroyCluster(ctx context.Context, orgSlug string, id string) error {
	cfg := config.FromContext(ctx)
	url := fmt.Sprintf("%s/api/v1/organizations/%s/postgres/%s", c.BaseURL(), orgSlug, id)

	req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Add("Authorization", "Bearer "+cfg.Tokens.GraphQL())
	req.Header.Add("Content-Type", "application/json")

	res, err := c.HTTPClient().Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()

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

	switch res.StatusCode {
	case http.StatusOK, http.StatusNoContent, http.StatusAccepted:
		return nil
	case http.StatusNotFound:
		return fmt.Errorf("cluster %s not found", id)
	case http.StatusForbidden:
		return fmt.Errorf("access denied: you don't have permission to destroy cluster %s", id)
	default:
		return fmt.Errorf("failed to destroy cluster (status %d): %s", res.StatusCode, string(body))
	}
}

// CreateAttachment creates a ManagedServiceAttachment record linking an app to a managed Postgres cluster
func (c *Client) CreateAttachment(ctx context.Context, clusterId string, input CreateAttachmentInput) (CreateAttachmentResponse, error) {
	var response CreateAttachmentResponse
	cfg := config.FromContext(ctx)
	url := fmt.Sprintf("%s/api/v1/postgres/%s/attachments", c.BaseURL(), clusterId)

	var buf bytes.Buffer
	if err := json.NewEncoder(&buf).Encode(input); err != nil {
		return response, fmt.Errorf("failed to encode request body: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf)
	if err != nil {
		return response, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Add("Authorization", "Bearer "+cfg.Tokens.GraphQL())
	req.Header.Add("Content-Type", "application/json")

	res, err := c.HTTPClient().Do(req)
	if err != nil {
		return response, err
	}
	defer res.Body.Close()

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

	switch res.StatusCode {
	case http.StatusOK, http.StatusCreated:
		if err = json.Unmarshal(body, &response); err != nil {
			return response, fmt.Errorf("failed to decode response: %w", err)
		}

		return response, nil
	case http.StatusNotFound:
		return response, fmt.Errorf("cluster %s not found", clusterId)
	case http.StatusForbidden:
		return response, fmt.Errorf("access denied: you don't have permission to attach cluster %s", clusterId)
	default:
		return response, fmt.Errorf("failed to create attachment (status %d): %s", res.StatusCode, string(body))
	}
}

// ListExtensions returns the catalog of available Postgres extensions for the
// given database, along with whether each one is currently installed.
func (c *Client) ListExtensions(ctx context.Context, id, database string) (ListExtensionsResponse, error) {
	var response ListExtensionsResponse
	cfg := config.FromContext(ctx)
	url := fmt.Sprintf("%s/api/v1/postgres/%s/databases/%s/extensions", c.BaseURL(), id, database)

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return response, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Add("Authorization", "Bearer "+cfg.Tokens.GraphQL())
	req.Header.Add("Content-Type", "application/json")

	res, err := c.HTTPClient().Do(req)
	if err != nil {
		return response, err
	}
	defer res.Body.Close()

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

	switch res.StatusCode {
	case http.StatusOK:
		if err = json.Unmarshal(body, &response); err != nil {
			return response, fmt.Errorf("failed to decode response: %w", err)
		}

		return response, nil
	case http.StatusNotFound:
		return response, fmt.Errorf("cluster %s or database %s not found", id, database)
	case http.StatusForbidden:
		return response, fmt.Errorf("access denied: you don't have permission to list extensions for cluster %s", id)
	default:
		return response, fmt.Errorf("failed to list extensions (status %d): %s", res.StatusCode, decodeError(body))
	}
}

// EnableExtension installs a Postgres extension in the given database.
func (c *Client) EnableExtension(ctx context.Context, id, database string, input EnableExtensionInput) error {
	cfg := config.FromContext(ctx)
	url := fmt.Sprintf("%s/api/v1/postgres/%s/databases/%s/extensions", c.BaseURL(), id, database)

	var buf bytes.Buffer
	if err := json.NewEncoder(&buf).Encode(input); err != nil {
		return fmt.Errorf("failed to encode request body: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf)
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Add("Authorization", "Bearer "+cfg.Tokens.GraphQL())
	req.Header.Add("Content-Type", "application/json")

	res, err := c.HTTPClient().Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()

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

	switch res.StatusCode {
	case http.StatusOK, http.StatusCreated, http.StatusNoContent:
		return nil
	case http.StatusConflict:
		return fmt.Errorf("%s", decodeError(body))
	case http.StatusBadRequest, http.StatusUnprocessableEntity:
		return fmt.Errorf("%s", decodeError(body))
	case http.StatusNotFound:
		return fmt.Errorf("cluster %s or database %s not found", id, database)
	case http.StatusForbidden:
		return fmt.Errorf("access denied: you don't have permission to enable extensions for cluster %s", id)
	default:
		return fmt.Errorf("failed to enable extension (status %d): %s", res.StatusCode, decodeError(body))
	}
}

// DisableExtension drops a Postgres extension from the given database.
func (c *Client) DisableExtension(ctx context.Context, id, database, name string, force bool) error {
	cfg := config.FromContext(ctx)
	url := fmt.Sprintf("%s/api/v1/postgres/%s/databases/%s/extensions/%s", c.BaseURL(), id, database, name)
	if force {
		url += "?force=true"
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Add("Authorization", "Bearer "+cfg.Tokens.GraphQL())
	req.Header.Add("Content-Type", "application/json")

	res, err := c.HTTPClient().Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()

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

	switch res.StatusCode {
	case http.StatusOK, http.StatusNoContent:
		return nil
	case http.StatusBadRequest, http.StatusUnprocessableEntity:
		return fmt.Errorf("%s", decodeError(body))
	case http.StatusNotFound:
		return fmt.Errorf("cluster %s or database %s not found", id, database)
	case http.StatusForbidden:
		return fmt.Errorf("access denied: you don't have permission to disable extensions for cluster %s", id)
	default:
		return fmt.Errorf("failed to disable extension (status %d): %s", res.StatusCode, decodeError(body))
	}
}

// decodeError extracts the "error" field from a UI-EX error response body, or
// falls back to the raw body string if the body is not a JSON error envelope.
func decodeError(body []byte) string {
	var envelope struct {
		Error string `json:"error"`
	}
	if err := json.Unmarshal(body, &envelope); err == nil && envelope.Error != "" {
		return envelope.Error
	}

	return string(body)
}

// DeleteAttachment removes a ManagedServiceAttachment record linking an app to a managed Postgres cluster
func (c *Client) DeleteAttachment(ctx context.Context, clusterId string, appName string) (DeleteAttachmentResponse, error) {
	var response DeleteAttachmentResponse
	cfg := config.FromContext(ctx)
	url := fmt.Sprintf("%s/api/v1/postgres/%s/attachments/%s", c.BaseURL(), clusterId, appName)

	req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
	if err != nil {
		return response, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Add("Authorization", "Bearer "+cfg.Tokens.GraphQL())

	res, err := c.HTTPClient().Do(req)
	if err != nil {
		return response, err
	}
	defer res.Body.Close()

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

	switch res.StatusCode {
	case http.StatusOK:
		if err = json.Unmarshal(body, &response); err != nil {
			return response, fmt.Errorf("failed to decode response: %w", err)
		}

		return response, nil
	case http.StatusNotFound:
		return response, fmt.Errorf("attachment not found for app '%s' on cluster %s", appName, clusterId)
	case http.StatusForbidden:
		return response, fmt.Errorf("access denied: you don't have permission to detach from cluster %s", clusterId)
	default:
		return response, fmt.Errorf("failed to delete attachment (status %d): %s", res.StatusCode, string(body))
	}
}
