package slack

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

	"github.com/nicholas-fedor/shoutrrr/pkg/format"
	"github.com/nicholas-fedor/shoutrrr/pkg/services/standard"
	"github.com/nicholas-fedor/shoutrrr/pkg/types"
	"github.com/nicholas-fedor/shoutrrr/pkg/util/jsonclient"
)

// apiPostMessage is the Slack API endpoint for sending messages.
const (
	apiPostMessage     = "https://slack.com/api/chat.postMessage"
	defaultHTTPTimeout = 10 * time.Second // defaultHTTPTimeout is the default timeout for HTTP requests.
)

// Service sends notifications to a pre-configured Slack channel or user.
type Service struct {
	standard.Standard
	Config *Config
	pkr    format.PropKeyResolver
	client *http.Client
}

// Send delivers a notification message to Slack.
func (service *Service) Send(message string, params *types.Params) error {
	config := service.Config

	if err := service.pkr.UpdateConfigFromParams(config, params); err != nil {
		return fmt.Errorf("updating config from params: %w", err)
	}

	payload := CreateJSONPayload(config, message)

	var err error
	if config.Token.IsAPIToken() {
		err = service.sendAPI(config, payload)
	} else {
		err = service.sendWebhook(config, payload)
	}

	if err != nil {
		return fmt.Errorf("failed to send slack notification: %w", err)
	}

	return nil
}

// Initialize configures the service with a URL and logger.
func (service *Service) Initialize(configURL *url.URL, logger types.StdLogger) error {
	service.SetLogger(logger)
	service.Config = &Config{}
	service.pkr = format.NewPropKeyResolver(service.Config)
	service.client = &http.Client{
		Timeout: defaultHTTPTimeout,
	}

	return service.Config.setURL(&service.pkr, configURL)
}

// GetID returns the service identifier.
func (service *Service) GetID() string {
	return Scheme
}

// sendAPI sends a notification using the Slack API.
func (service *Service) sendAPI(config *Config, payload any) error {
	response := APIResponse{}
	jsonClient := jsonclient.NewClient()
	jsonClient.Headers().Set("Authorization", config.Token.Authorization())

	if err := jsonClient.Post(apiPostMessage, payload, &response); err != nil {
		return fmt.Errorf("posting to Slack API: %w", err)
	}

	if !response.Ok {
		if response.Error != "" {
			return fmt.Errorf("%w: %v", ErrAPIResponseFailure, response.Error)
		}

		return ErrUnknownAPIError
	}

	if response.Warning != "" {
		service.Logf("Slack API warning: %q", response.Warning)
	}

	return nil
}

// sendWebhook sends a notification using a Slack webhook.
func (service *Service) sendWebhook(config *Config, payload any) error {
	payloadBytes, err := json.Marshal(payload)
	if err != nil {
		return fmt.Errorf("failed to marshal payload: %w", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), defaultHTTPTimeout)
	defer cancel()

	req, err := http.NewRequestWithContext(
		ctx,
		http.MethodPost,
		config.Token.WebhookURL(),
		bytes.NewBuffer(payloadBytes),
	)
	if err != nil {
		return fmt.Errorf("failed to create webhook request: %w", err)
	}

	req.Header.Set("Content-Type", jsonclient.ContentType)

	res, err := service.client.Do(req)
	if err != nil {
		return fmt.Errorf("failed to invoke webhook: %w", err)
	}

	defer res.Body.Close()
	resBytes, _ := io.ReadAll(res.Body)
	response := string(resBytes)

	switch response {
	case "":
		if res.StatusCode != http.StatusOK {
			return fmt.Errorf("%w: %v", ErrWebhookStatusFailure, res.Status)
		}

		fallthrough
	case "ok":
		return nil
	default:
		return fmt.Errorf("%w: %v", ErrWebhookResponseFailure, response)
	}
}
