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"
	"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 = "documents.get"
	documentsDialTimeout  = 5 * time.Second
	documentsCallTimeout  = 10 * time.Second
)

func (a *PaymentAPI) getActDocument(r *http.Request, account *model.Account, _ *sresponse.TokenData) 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 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 response.Auto(a.logger, a.Name(), err)
	}
	if !allowed {
		a.logger.Debug("Access denied when downloading act", mutil.PLog(a.oph, r))
		return response.AccessDenied(a.logger, a.Name(), "payments read permission denied")
	}

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

	if a.discovery == nil {
		return 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 response.Auto(a.logger, a.Name(), err)
	}

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

	docResp, err := a.fetchActDocument(ctx, service.InvokeURI, paymentRef)
	if err != nil {
		a.logger.Warn("Failed to fetch act document", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
		return documentErrorResponse(a.logger, a.Name(), err)
	}
	if len(docResp.GetContent()) == 0 {
		return response.Error(a.logger, a.Name(), http.StatusInternalServerError, "empty_document", "document service returned empty payload")
	}

	filename := strings.TrimSpace(docResp.GetFilename())
	if filename == "" {
		filename = fmt.Sprintf("act_%s.pdf", paymentRef)
	}
	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)
		if _, writeErr := w.Write(docResp.GetContent()); writeErr != nil {
			a.logger.Warn("Failed to write document response", zap.Error(writeErr))
		}
	}
}

func (a *PaymentAPI) fetchActDocument(ctx context.Context, invokeURI, paymentRef string) (*documentsv1.GetDocumentResponse, error) {
	dialCtx, cancel := context.WithTimeout(ctx, documentsDialTimeout)
	defer cancel()

	conn, err := grpc.DialContext(dialCtx, invokeURI, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		return nil, merrors.InternalWrap(err, "dial billing documents")
	}
	defer conn.Close()

	client := documentsv1.NewDocumentServiceClient(conn)

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

	return client.GetDocument(callCtx, &documentsv1.GetDocumentRequest{
		PaymentRef: paymentRef,
		Type:       documentsv1.DocumentType_DOCUMENT_TYPE_ACT,
	})
}

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