package monetix

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

	"github.com/tech/sendico/pkg/merrors"
	"go.uber.org/zap"
)

const (
	outcomeAccepted     = "accepted"
	outcomeHTTPError    = "http_error"
	outcomeNetworkError = "network_error"
)

// sendCardPayout dispatches a PAN-based payout.
func (c *Client) sendCardPayout(ctx context.Context, req CardPayoutRequest) (*CardPayoutSendResult, error) {
	maskedPAN := MaskPAN(req.Card.PAN)
	return c.send(ctx, &req, "/v2/payment/card/payout",
		func() {
			c.logger.Info("Dispatching Monetix card payout",
				zap.String("payout_id", req.General.PaymentID),
				zap.Int64("amount_minor", req.Payment.Amount),
				zap.String("currency", req.Payment.Currency),
				zap.String("pan", maskedPAN),
			)
		},
		func(r *CardPayoutSendResult) {
			c.logger.Info("Monetix payout response",
				zap.String("payout_id", req.General.PaymentID),
				zap.Bool("accepted", r.Accepted),
				zap.Int("status_code", r.StatusCode),
				zap.String("provider_request_id", r.ProviderRequestID),
				zap.String("error_code", r.ErrorCode),
				zap.String("error_message", r.ErrorMessage),
			)
		})
}

// sendCardTokenPayout dispatches a token-based payout.
func (c *Client) sendCardTokenPayout(ctx context.Context, req CardTokenPayoutRequest) (*CardPayoutSendResult, error) {
	return c.send(ctx, &req, "/v2/payment/card/payout/token",
		func() {
			c.logger.Info("Dispatching Monetix card token payout",
				zap.String("payout_id", req.General.PaymentID),
				zap.Int64("amount_minor", req.Payment.Amount),
				zap.String("currency", req.Payment.Currency),
				zap.String("masked_pan", req.Token.MaskedPAN),
			)
		},
		func(r *CardPayoutSendResult) {
			c.logger.Info("Monetix token payout response",
				zap.String("payout_id", req.General.PaymentID),
				zap.Bool("accepted", r.Accepted),
				zap.Int("status_code", r.StatusCode),
				zap.String("provider_request_id", r.ProviderRequestID),
				zap.String("error_code", r.ErrorCode),
				zap.String("error_message", r.ErrorMessage),
			)
		})
}

// sendTokenization sends a tokenization request.
func (c *Client) sendTokenization(ctx context.Context, req CardTokenizeRequest) (*TokenizationResult, error) {
	if ctx == nil {
		ctx = context.Background()
	}
	if c == nil {
		return nil, merrors.Internal("monetix client not initialised")
	}
	if strings.TrimSpace(c.cfg.SecretKey) == "" {
		return nil, merrors.Internal("monetix secret key not configured")
	}
	if strings.TrimSpace(c.cfg.BaseURL) == "" {
		return nil, merrors.Internal("monetix base url not configured")
	}

	req.General.Signature = ""
	signature, err := signPayload(req, c.cfg.SecretKey)
	if err != nil {
		return nil, merrors.Internal("failed to sign request: " + err.Error())
	}
	req.General.Signature = signature

	payload, err := json.Marshal(req)
	if err != nil {
		return nil, merrors.Internal("failed to marshal request payload: " + err.Error())
	}

	url := strings.TrimRight(c.cfg.BaseURL, "/") + "/v1/tokenize"
	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload))
	if err != nil {
		return nil, merrors.Internal("failed to build request: " + err.Error())
	}
	httpReq.Header.Set("Content-Type", "application/json")
	httpReq.Header.Set("Accept", "application/json")

	c.logger.Info("Dispatching Monetix card tokenization",
		zap.String("request_id", req.General.PaymentID),
		zap.String("masked_pan", MaskPAN(req.Card.PAN)),
	)

	logRequestDeadline(c.logger, ctx, url)
	start := time.Now()
	resp, err := c.client.Do(httpReq)
	duration := time.Since(start)
	if err != nil {
		observeRequest(outcomeNetworkError, duration)
		fields := []zap.Field{
			zap.String("url", url),
			zap.Error(err),
		}
		if ctxErr := ctx.Err(); ctxErr != nil {
			fields = append(fields, zap.NamedError("ctx_error", ctxErr))
		}
		if deadline, ok := ctx.Deadline(); ok {
			fields = append(fields, zap.Time("deadline", deadline), zap.Duration("time_until_deadline", time.Until(deadline)))
		}
		c.logger.Warn("Monetix tokenization request failed", fields...)
		return nil, merrors.Internal("monetix tokenization request failed: " + err.Error())
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	outcome := outcomeAccepted
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		outcome = outcomeHTTPError
	}
	observeRequest(outcome, duration)

	result := &TokenizationResult{
		CardPayoutSendResult: CardPayoutSendResult{
			Accepted:   resp.StatusCode >= 200 && resp.StatusCode < 300,
			StatusCode: resp.StatusCode,
		},
	}

	var apiResp APIResponse
	if len(body) > 0 {
		if err := json.Unmarshal(body, &apiResp); err != nil {
			c.logger.Warn("Failed to decode Monetix tokenization response", zap.String("request_id", req.General.PaymentID), zap.Int("status_code", resp.StatusCode), zap.Error(err))
		} else {
			var tokenData struct {
				Token       string `json:"token"`
				MaskedPAN   string `json:"masked_pan"`
				ExpiryMonth string `json:"expiry_month"`
				ExpiryYear  string `json:"expiry_year"`
				CardBrand   string `json:"card_brand"`
			}
			_ = json.Unmarshal(body, &tokenData)
			result.Token = tokenData.Token
			result.MaskedPAN = tokenData.MaskedPAN
			result.ExpiryMonth = tokenData.ExpiryMonth
			result.ExpiryYear = tokenData.ExpiryYear
			result.CardBrand = tokenData.CardBrand
		}
	}

	if apiResp.Operation.RequestID != "" {
		result.ProviderRequestID = apiResp.Operation.RequestID
	} else if apiResp.RequestID != "" {
		result.ProviderRequestID = apiResp.RequestID
	}

	if !result.Accepted {
		result.ErrorCode = apiResp.Code
		if result.ErrorCode == "" {
			result.ErrorCode = http.StatusText(resp.StatusCode)
		}
		result.ErrorMessage = apiResp.Message
		if result.ErrorMessage == "" {
			result.ErrorMessage = apiResp.Operation.Message
		}
	}

	c.logger.Info("Monetix tokenization response",
		zap.String("request_id", req.General.PaymentID),
		zap.Bool("accepted", result.Accepted),
		zap.Int("status_code", resp.StatusCode),
		zap.String("provider_request_id", result.ProviderRequestID),
		zap.String("error_code", result.ErrorCode),
		zap.String("error_message", result.ErrorMessage),
	)

	return result, nil
}

func (c *Client) send(ctx context.Context, req any, path string, dispatchLog func(), responseLog func(*CardPayoutSendResult)) (*CardPayoutSendResult, error) {
	if ctx == nil {
		ctx = context.Background()
	}
	if c == nil {
		return nil, merrors.Internal("monetix client not initialised")
	}
	if strings.TrimSpace(c.cfg.SecretKey) == "" {
		return nil, merrors.Internal("monetix secret key not configured")
	}
	if strings.TrimSpace(c.cfg.BaseURL) == "" {
		return nil, merrors.Internal("monetix base url not configured")
	}

	setSignature, err := clearSignature(req)
	if err != nil {
		return nil, err
	}
	signature, err := signPayload(req, c.cfg.SecretKey)
	if err != nil {
		return nil, merrors.Internal("failed to sign request: " + err.Error())
	}
	setSignature(signature)

	payload, err := json.Marshal(req)
	if err != nil {
		return nil, merrors.Internal("failed to marshal request payload: " + err.Error())
	}

	url := strings.TrimRight(c.cfg.BaseURL, "/") + path
	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload))
	if err != nil {
		return nil, merrors.Internal("failed to build request: " + err.Error())
	}
	httpReq.Header.Set("Content-Type", "application/json")
	httpReq.Header.Set("Accept", "application/json")

	if dispatchLog != nil {
		dispatchLog()
	}

	logRequestDeadline(c.logger, ctx, url)
	start := time.Now()
	resp, err := c.client.Do(httpReq)
	duration := time.Since(start)
	if err != nil {
		observeRequest(outcomeNetworkError, duration)
		fields := []zap.Field{
			zap.String("url", url),
			zap.Error(err),
		}
		if ctxErr := ctx.Err(); ctxErr != nil {
			fields = append(fields, zap.NamedError("ctx_error", ctxErr))
		}
		if deadline, ok := ctx.Deadline(); ok {
			fields = append(fields, zap.Time("deadline", deadline), zap.Duration("time_until_deadline", time.Until(deadline)))
		}
		c.logger.Warn("Monetix request failed", fields...)
		return nil, merrors.Internal("monetix request failed: " + err.Error())
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	outcome := outcomeAccepted
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		outcome = outcomeHTTPError
	}
	observeRequest(outcome, duration)

	result := &CardPayoutSendResult{
		Accepted:   resp.StatusCode >= 200 && resp.StatusCode < 300,
		StatusCode: resp.StatusCode,
	}

	var apiResp APIResponse
	if len(body) > 0 {
		if err := json.Unmarshal(body, &apiResp); err != nil {
			c.logger.Warn("Failed to decode Monetix response", zap.Int("status_code", resp.StatusCode), zap.Error(err))
		}
	}

	if apiResp.Operation.RequestID != "" {
		result.ProviderRequestID = apiResp.Operation.RequestID
	} else if apiResp.RequestID != "" {
		result.ProviderRequestID = apiResp.RequestID
	}

	if !result.Accepted {
		result.ErrorCode = apiResp.Code
		if result.ErrorCode == "" {
			result.ErrorCode = http.StatusText(resp.StatusCode)
		}
		result.ErrorMessage = apiResp.Message
		if result.ErrorMessage == "" {
			result.ErrorMessage = apiResp.Operation.Message
		}
	}

	if responseLog != nil {
		responseLog(result)
	}

	return result, nil
}

func clearSignature(req any) (func(string), error) {
	switch r := req.(type) {
	case *CardPayoutRequest:
		r.General.Signature = ""
		return func(sig string) { r.General.Signature = sig }, nil
	case *CardTokenPayoutRequest:
		r.General.Signature = ""
		return func(sig string) { r.General.Signature = sig }, nil
	case *CardTokenizeRequest:
		r.General.Signature = ""
		return func(sig string) { r.General.Signature = sig }, nil
	default:
		return nil, merrors.Internal("unsupported monetix payload type for signing")
	}
}

func logRequestDeadline(logger *zap.Logger, ctx context.Context, url string) {
	if logger == nil {
		return
	}
	if ctx == nil {
		logger.Info("Monetix request context is nil", zap.String("url", url))
		return
	}
	deadline, ok := ctx.Deadline()
	if !ok {
		logger.Info("Monetix request context has no deadline", zap.String("url", url))
		return
	}
	logger.Info("Monetix request context deadline",
		zap.String("url", url),
		zap.Time("deadline", deadline),
		zap.Duration("time_until_deadline", time.Until(deadline)),
	)
}
