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"
)

// Service sends notifications to a pre-configured Slack channel or user.
type Service struct {
	standard.Standard

	Config     *Config
	pkr        format.PropKeyResolver
	client     *http.Client
	httpClient types.HTTPClient
}

// 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.
)

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

// Initialize configures the service with a URL and logger.
func (s *Service) Initialize(serviceURL *url.URL, logger types.StdLogger) error {
	s.SetLogger(logger)
	s.Config = &Config{}

	s.pkr = format.NewPropKeyResolver(s.Config)
	if s.client == nil {
		s.client = &http.Client{
			Timeout: defaultHTTPTimeout,
		}
	}

	return s.Config.setURL(&s.pkr, serviceURL)
}

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

	if err := s.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 = s.sendAPI(config, payload)
	} else {
		err = s.sendWebhook(config, payload)
	}

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

	return nil
}

// SetHTTPClient sets a custom HTTP client for the service.
func (s *Service) SetHTTPClient(client types.HTTPClient) {
	s.httpClient = client
}

// httpClientOrDefault returns the injected client or the internal client.
func (s *Service) httpClientOrDefault() types.HTTPClient {
	if s.httpClient != nil {
		return s.httpClient
	}

	return s.client
}

// sendAPI sends a notification using the Slack API.
func (s *Service) sendAPI(config *Config, payload any) error {
	response := APIResponse{}
	jsonClient := jsonclient.NewWithHTTPClient(s.httpClientOrDefault())
	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 != "" {
		s.Logf("Slack API warning: %q", response.Warning)
	}

	return nil
}

// sendWebhook sends a notification using a Slack webhook.
func (s *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)

	client := s.httpClientOrDefault()

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

	defer func() { _ = res.Body.Close() }()

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

	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)
	}
}
