package paymentapiimp

import (
	"context"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/tech/sendico/pkg/api/http/response"
	"github.com/tech/sendico/pkg/discovery"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/model"
	"github.com/tech/sendico/pkg/mservice"
	"github.com/tech/sendico/pkg/mutil/mzap"
	documentsv1 "github.com/tech/sendico/pkg/proto/billing/documents/v1"
	connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
	endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
	orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
	"github.com/tech/sendico/server/interface/api/sresponse"
	mutil "github.com/tech/sendico/server/internal/mutil/param"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/status"
)

const (
	documentsServiceName  = "BILLING_DOCUMENTS"
	documentsOperationGet = discovery.OperationDocumentsGet
	documentsCallTimeout  = 10 * time.Second
	gatewayCallTimeout    = 10 * time.Second
)

var allowedOperationGatewayServices = map[mservice.Type]struct{}{
	mservice.ChainGateway:   {},
	mservice.TronGateway:    {},
	mservice.MntxGateway:    {},
	mservice.PaymentGateway: {},
	mservice.TgSettle:       {},
}

func (a *PaymentAPI) getOperationDocument(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
	orgRef, denied := a.authorizeDocumentDownload(r, account)
	if denied != nil {
		return denied
	}

	query := r.URL.Query()
	gatewayService := normalizeGatewayService(query.Get("gateway_service"))
	if gatewayService == "" {
		return response.BadRequest(a.logger, a.Name(), "missing_parameter", "gateway_service is required")
	}
	if _, ok := allowedOperationGatewayServices[gatewayService]; !ok {
		return response.BadRequest(a.logger, a.Name(), "invalid_parameter", "unsupported gateway_service")
	}

	operationRef := strings.TrimSpace(query.Get("operation_ref"))
	if operationRef == "" {
		return response.BadRequest(a.logger, a.Name(), "missing_parameter", "operation_ref is required")
	}
	paymentRef := strings.TrimSpace(query.Get("payment_ref"))
	if paymentRef == "" {
		return response.BadRequest(a.logger, a.Name(), "missing_parameter", "payment_ref is required")
	}

	service, gateway, h := a.resolveOperationDocumentDeps(r.Context(), gatewayService)
	if h != nil {
		return h
	}

	op, err := a.fetchGatewayOperation(r.Context(), gateway.InvokeURI, operationRef)
	if err != nil {
		a.logger.Warn("Failed to fetch gateway operation for document generation", zap.Error(err), mzap.ObjRef("organization_ref", orgRef), zap.String("gateway_service", gatewayService), zap.String("operation_ref", operationRef))
		return documentErrorResponse(a.logger, a.Name(), err)
	}

	req := operationDocumentRequest(orgRef.Hex(), gatewayService, operationRef, paymentRef, op)
	if payment, paymentErr := a.fetchPayment(r.Context(), orgRef.Hex(), paymentRef); paymentErr != nil {
		a.logger.Warn(
			"Failed to fetch payment snapshot for operation document",
			zap.Error(paymentErr),
			mzap.ObjRef("organization_ref", orgRef),
			zap.String("payment_ref", paymentRef),
			zap.String("operation_ref", operationRef),
		)
	} else {
		enrichOperationDocumentRequestFromPayment(req, payment)
	}

	docResp, err := a.fetchOperationDocument(r.Context(), service.InvokeURI, req)
	if err != nil {
		a.logger.Warn("Failed to fetch operation document", zap.Error(err), mzap.ObjRef("organization_ref", orgRef), zap.String("gateway_service", gatewayService), zap.String("operation_ref", operationRef))
		return documentErrorResponse(a.logger, a.Name(), err)
	}

	return operationDocumentResponse(a.logger, a.Name(), docResp, fmt.Sprintf("operation_%s.pdf", sanitizeFilenameComponent(operationRef)))
}

func (a *PaymentAPI) authorizeDocumentDownload(r *http.Request, account *model.Account) (bson.ObjectID, http.HandlerFunc) {
	orgRef, err := a.oph.GetRef(r)
	if err != nil {
		a.logger.Warn("Failed to parse organization reference for document request", zap.Error(err), mutil.PLog(a.oph, r))
		return bson.NilObjectID, response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
	}

	ctx := r.Context()
	allowed, err := a.enf.Enforce(ctx, a.permissionRef, account.ID, orgRef, bson.NilObjectID, model.ActionRead)
	if err != nil {
		a.logger.Warn("Failed to check payments access permissions", zap.Error(err), mutil.PLog(a.oph, r))
		return bson.NilObjectID, response.Auto(a.logger, a.Name(), err)
	}
	if !allowed {
		a.logger.Debug("Access denied when downloading document", mutil.PLog(a.oph, r))
		return bson.NilObjectID, response.AccessDenied(a.logger, a.Name(), "payments read permission denied")
	}

	return orgRef, nil
}

func (a *PaymentAPI) resolveOperationDocumentDeps(ctx context.Context, gatewayService mservice.Type) (*discovery.ServiceSummary, *discovery.GatewaySummary, http.HandlerFunc) {
	if a.discovery == nil {
		return nil, nil, response.Error(a.logger, a.Name(), http.StatusServiceUnavailable, "service_unavailable", "discovery client is not configured")
	}

	lookupCtx, cancel := context.WithTimeout(ctx, discoveryLookupTimeout)
	defer cancel()

	lookupResp, err := a.discovery.Lookup(lookupCtx)
	if err != nil {
		a.logger.Warn("Failed to lookup discovery registry", zap.Error(err))
		return nil, nil, response.Auto(a.logger, a.Name(), err)
	}

	service := findDocumentsService(lookupResp.Services)
	if service == nil {
		return nil, nil, response.Error(a.logger, a.Name(), http.StatusServiceUnavailable, "service_unavailable", "billing documents service unavailable")
	}

	gateway := findGatewayForService(lookupResp.Gateways, gatewayService)
	if gateway == nil {
		return nil, nil, response.Error(a.logger, a.Name(), http.StatusServiceUnavailable, "service_unavailable", "gateway service unavailable")
	}

	return service, gateway, nil
}

func operationDocumentResponse(logger mlogger.Logger, source mservice.Type, docResp *documentsv1.GetDocumentResponse, fallbackFilename string) http.HandlerFunc {
	if docResp == nil || len(docResp.GetContent()) == 0 {
		return response.Error(logger, source, http.StatusInternalServerError, "empty_document", "document service returned empty payload")
	}

	filename := strings.TrimSpace(docResp.GetFilename())
	if filename == "" {
		filename = strings.TrimSpace(fallbackFilename)
	}
	if filename == "" {
		filename = "document.pdf"
	}

	mimeType := strings.TrimSpace(docResp.GetMimeType())
	if mimeType == "" {
		mimeType = "application/pdf"
	}

	return func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", mimeType)
		w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
		w.WriteHeader(http.StatusOK)
		//nolint:gosec // Binary payload is served as attachment with explicit content type.
		if _, err := w.Write(docResp.GetContent()); err != nil {
			logger.Warn("Failed to write document response", zap.Error(err))
		}
	}
}

func normalizeGatewayService(raw string) mservice.Type {
	value := strings.ToLower(strings.TrimSpace(raw))
	if value == "" {
		return ""
	}

	switch value {
	case mservice.ChainGateway:
		return mservice.ChainGateway
	case mservice.TronGateway:
		return mservice.TronGateway
	case mservice.MntxGateway:
		return mservice.MntxGateway
	case mservice.PaymentGateway:
		return mservice.PaymentGateway
	case mservice.TgSettle:
		return mservice.TgSettle
	default:
		return ""
	}
}

func sanitizeFilenameComponent(value string) string {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return ""
	}

	var b strings.Builder
	b.Grow(len(trimmed))

	for _, r := range trimmed {
		switch {
		case r >= 'a' && r <= 'z':
			b.WriteRune(r)
		case r >= 'A' && r <= 'Z':
			b.WriteRune(r)
		case r >= '0' && r <= '9':
			b.WriteRune(r)
		case r == '-', r == '_':
			b.WriteRune(r)
		default:
			b.WriteRune('_')
		}
	}

	clean := strings.Trim(b.String(), "_")
	if clean == "" {
		return "operation"
	}

	return clean
}

func (a *PaymentAPI) fetchOperationDocument(ctx context.Context, invokeURI string, req *documentsv1.GetOperationDocumentRequest) (*documentsv1.GetDocumentResponse, error) {
	conn, err := grpc.NewClient(invokeURI, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		return nil, merrors.InternalWrap(err, "dial billing documents")
	}
	defer func() {
		if closeErr := conn.Close(); closeErr != nil {
			a.logger.Warn("Failed to close billing documents connection", zap.Error(closeErr))
		}
	}()

	client := documentsv1.NewDocumentServiceClient(conn)

	callCtx, callCancel := context.WithTimeout(ctx, documentsCallTimeout)
	defer callCancel()

	return client.GetOperationDocument(callCtx, req)
}

func (a *PaymentAPI) fetchGatewayOperation(ctx context.Context, invokeURI, operationRef string) (*connectorv1.Operation, error) {
	conn, err := grpc.NewClient(invokeURI, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		return nil, merrors.InternalWrap(err, "dial gateway connector")
	}
	defer func() {
		if closeErr := conn.Close(); closeErr != nil {
			a.logger.Warn("Failed to close gateway connector connection", zap.Error(closeErr))
		}
	}()

	client := connectorv1.NewConnectorServiceClient(conn)

	callCtx, callCancel := context.WithTimeout(ctx, gatewayCallTimeout)
	defer callCancel()

	resp, err := client.GetOperation(callCtx, &connectorv1.GetOperationRequest{OperationId: strings.TrimSpace(operationRef)})
	if err != nil {
		return nil, err
	}

	op := resp.GetOperation()
	if op == nil {
		return nil, merrors.NoData("gateway returned empty operation payload")
	}

	return op, nil
}

func (a *PaymentAPI) fetchPayment(ctx context.Context, organizationRef, paymentRef string) (*orchestrationv2.Payment, error) {
	if a.execution == nil {
		return nil, merrors.Internal("payment execution client is not configured")
	}

	resp, err := a.execution.GetPayment(ctx, &orchestrationv2.GetPaymentRequest{
		Meta:       requestMeta(organizationRef, ""),
		PaymentRef: strings.TrimSpace(paymentRef),
	})
	if err != nil {
		return nil, err
	}
	if resp == nil {
		return nil, merrors.NoData("payment orchestrator returned empty response")
	}
	if resp.GetPayment() == nil {
		return nil, merrors.NoData("payment orchestrator returned empty payment")
	}

	return resp.GetPayment(), nil
}

func findGatewayForService(gateways []discovery.GatewaySummary, gatewayService mservice.Type) *discovery.GatewaySummary {
	candidates := make([]discovery.GatewaySummary, 0, len(gateways))
	for _, gw := range gateways {
		if !gw.Healthy || strings.TrimSpace(gw.InvokeURI) == "" {
			continue
		}

		rail := discovery.NormalizeRail(gw.Rail)
		network := strings.ToLower(strings.TrimSpace(gw.Network))
		switch gatewayService {
		case mservice.MntxGateway:
			if rail == discovery.NormalizeRail(discovery.RailCardPayout) {
				candidates = append(candidates, gw)
			}
		case mservice.PaymentGateway, mservice.TgSettle:
			if rail == discovery.NormalizeRail(discovery.RailProviderSettlement) {
				candidates = append(candidates, gw)
			}
		case mservice.TronGateway:
			if rail == discovery.NormalizeRail(discovery.RailCrypto) && strings.Contains(network, "tron") {
				candidates = append(candidates, gw)
			}
		case mservice.ChainGateway:
			if rail == discovery.NormalizeRail(discovery.RailCrypto) && !strings.Contains(network, "tron") {
				candidates = append(candidates, gw)
			}
		}
	}

	if len(candidates) == 0 && gatewayService == mservice.ChainGateway {
		for _, gw := range gateways {
			if gw.Healthy && strings.TrimSpace(gw.InvokeURI) != "" && discovery.NormalizeRail(gw.Rail) == discovery.NormalizeRail(discovery.RailCrypto) {
				candidates = append(candidates, gw)
			}
		}
	}
	if len(candidates) == 0 {
		return nil
	}

	best := candidates[0]
	for _, candidate := range candidates[1:] {
		if candidate.RoutingPriority > best.RoutingPriority {
			best = candidate
		}
	}

	return &best
}

func operationDocumentRequest(organizationRef string, gatewayService mservice.Type, requestedOperationRef string, paymentRef string, op *connectorv1.Operation) *documentsv1.GetOperationDocumentRequest {
	req := &documentsv1.GetOperationDocumentRequest{
		OrganizationRef: strings.TrimSpace(organizationRef),
		GatewayService:  gatewayService,
		OperationRef:    firstNonEmpty(strings.TrimSpace(op.GetOperationRef()), strings.TrimSpace(requestedOperationRef)),
		PaymentRef:      strings.TrimSpace(paymentRef),
		OperationCode:   strings.TrimSpace(op.GetType().String()),
		OperationLabel:  strings.TrimSpace(op.GetType().String()),
		OperationState:  strings.TrimSpace(op.GetStatus().String()),
		Amount:          strings.TrimSpace(op.GetMoney().GetAmount()),
		Currency:        strings.TrimSpace(op.GetMoney().GetCurrency()),
	}

	if ts := op.GetCreatedAt(); ts != nil {
		req.StartedAtUnixMs = ts.AsTime().UnixMilli()
	}
	if ts := op.GetUpdatedAt(); ts != nil {
		req.CompletedAtUnixMs = ts.AsTime().UnixMilli()
	}

	if isFailedOperationStatus(op.GetStatus()) {
		req.FailureCode = strings.TrimSpace(op.GetStatus().String())
	}

	return req
}

func isFailedOperationStatus(status connectorv1.OperationStatus) bool {
	return status == connectorv1.OperationStatus_OPERATION_FAILED || status == connectorv1.OperationStatus_OPERATION_CANCELLED
}

func enrichOperationDocumentRequestFromPayment(req *documentsv1.GetOperationDocumentRequest, payment *orchestrationv2.Payment) {
	if req == nil || payment == nil {
		return
	}

	req.PaymentRef = firstNonEmpty(strings.TrimSpace(req.GetPaymentRef()), strings.TrimSpace(payment.GetPaymentRef()))
	req.ClientName = firstNonEmpty(strings.TrimSpace(req.GetClientName()), paymentClientName(payment))
}

func paymentClientName(payment *orchestrationv2.Payment) string {
	if payment == nil {
		return ""
	}

	intent := payment.GetIntentSnapshot()
	if intent == nil {
		return ""
	}

	if customerDescription := strings.TrimSpace(intent.GetComment()); customerDescription != "" {
		return customerDescription
	}

	return firstNonEmpty(
		paymentEndpointClientName(intent.GetDestination()),
		paymentEndpointClientName(intent.GetSource()),
	)
}

func paymentEndpointClientName(endpoint *endpointv1.PaymentEndpoint) string {
	if endpoint == nil {
		return ""
	}

	method := endpoint.GetPaymentMethod()
	if method == nil {
		return ""
	}

	switch method.GetType() {
	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD:
		type cardMethodData struct {
			FirstName string `bson:"firstName"`
			LastName  string `bson:"lastName"`
		}
		var payload cardMethodData
		if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
			return ""
		}
		return strings.TrimSpace(strings.Join([]string{
			strings.TrimSpace(payload.FirstName),
			strings.TrimSpace(payload.LastName),
		}, " "))

	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_BANK_ACCOUNT:
		type bankMethodData struct {
			RecipientName string `bson:"recipientName"`
		}
		var payload bankMethodData
		if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
			return ""
		}
		return strings.TrimSpace(payload.RecipientName)

	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_IBAN:
		type ibanMethodData struct {
			AccountHolder string `bson:"accountHolder"`
		}
		var payload ibanMethodData
		if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
			return ""
		}
		return strings.TrimSpace(payload.AccountHolder)

	default:
		return ""
	}
}

func findDocumentsService(services []discovery.ServiceSummary) *discovery.ServiceSummary {
	for _, svc := range services {
		if !strings.EqualFold(svc.Service, documentsServiceName) {
			continue
		}
		if !svc.Healthy || strings.TrimSpace(svc.InvokeURI) == "" {
			continue
		}
		if len(svc.Ops) == 0 || hasOperation(svc.Ops, documentsOperationGet) {
			return &svc
		}
	}
	return nil
}

func hasOperation(ops []string, target string) bool {
	for _, op := range ops {
		if strings.EqualFold(strings.TrimSpace(op), target) {
			return true
		}
	}
	return false
}

func documentErrorResponse(logger mlogger.Logger, source mservice.Type, err error) http.HandlerFunc {
	statusErr, ok := status.FromError(err)
	if !ok {
		return response.Internal(logger, source, err)
	}

	switch statusErr.Code() {
	case codes.InvalidArgument:
		return response.BadRequest(logger, source, "invalid_argument", statusErr.Message())
	case codes.NotFound:
		return response.NotFound(logger, source, statusErr.Message())
	case codes.Unimplemented:
		return response.NotImplemented(logger, source, statusErr.Message())
	case codes.FailedPrecondition:
		return response.Error(logger, source, http.StatusPreconditionFailed, "failed_precondition", statusErr.Message())
	case codes.Unavailable:
		return response.Error(logger, source, http.StatusServiceUnavailable, "service_unavailable", statusErr.Message())
	default:
		return response.Internal(logger, source, err)
	}
}
