package documents

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"strings"
	"time"

	"github.com/tech/sendico/billing/documents/internal/appversion"
	"github.com/tech/sendico/billing/documents/internal/content"
	"github.com/tech/sendico/billing/documents/internal/docstore"
	"github.com/tech/sendico/billing/documents/renderer"
	"github.com/tech/sendico/billing/documents/storage"
	"github.com/tech/sendico/billing/documents/storage/model"
	"github.com/tech/sendico/pkg/api/routers"
	"github.com/tech/sendico/pkg/discovery"
	msg "github.com/tech/sendico/pkg/messaging"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/mservice"
	documentsv1 "github.com/tech/sendico/pkg/proto/billing/documents/v1"
	"go.uber.org/zap"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// TemplateRenderer renders the acceptance template into tagged blocks.
type TemplateRenderer interface {
	Render(snapshot model.ActSnapshot) ([]renderer.Block, error)
}

// Option configures the documents service.
type Option func(*Service)

// WithDiscoveryInvokeURI configures the discovery invoke URI.
func WithDiscoveryInvokeURI(uri string) Option {
	return func(s *Service) {
		if s == nil {
			return
		}

		s.invokeURI = strings.TrimSpace(uri)
	}
}

// WithProducer sets the messaging producer.
func WithProducer(producer msg.Producer) Option {
	return func(s *Service) {
		if s == nil {
			return
		}

		s.producer = producer
	}
}

// WithConfig sets the service config.
func WithConfig(cfg Config) Option {
	return func(s *Service) {
		if s == nil {
			return
		}

		s.config = cfg
	}
}

// WithDocumentStore sets the document storage backend.
func WithDocumentStore(store docstore.Store) Option {
	return func(s *Service) {
		if s == nil {
			return
		}

		s.docStore = store
	}
}

// WithTemplateRenderer overrides the template renderer (useful for tests).
func WithTemplateRenderer(renderer TemplateRenderer) Option {
	return func(s *Service) {
		if s == nil {
			return
		}

		s.template = renderer
	}
}

// Service provides billing document metadata and retrieval endpoints.
type Service struct {
	documentsv1.UnimplementedDocumentServiceServer

	logger    mlogger.Logger
	storage   storage.Repository
	docStore  docstore.Store
	producer  msg.Producer
	announcer *discovery.Announcer
	invokeURI string
	config    Config
	template  TemplateRenderer
}

// NewService constructs a documents service with optional configuration.
func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Producer, opts ...Option) *Service {
	initMetrics()

	svc := &Service{
		logger:   logger.Named("documents"),
		storage:  repo,
		producer: producer,
	}
	for _, opt := range opts {
		opt(svc)
	}

	if svc.template == nil {
		if tmpl, err := newTemplateRenderer(svc.config.AcceptanceTemplatePath()); err != nil {
			svc.logger.Warn("Failed to load acceptance template", zap.Error(err))
		} else {
			svc.template = tmpl
		}
	}

	svc.startDiscoveryAnnouncer()

	return svc
}

func (s *Service) Register(router routers.GRPC) error {
	return router.Register(func(reg grpc.ServiceRegistrar) {
		documentsv1.RegisterDocumentServiceServer(reg, s)
	})
}

func (s *Service) Shutdown() {
	if s == nil {
		return
	}

	if s.announcer != nil {
		s.announcer.Stop()
	}
}

func (s *Service) GetOperationDocument(_ context.Context, req *documentsv1.GetOperationDocumentRequest) (resp *documentsv1.GetDocumentResponse, err error) {
	start := time.Now()
	organizationRef := ""
	gatewayService := ""
	operationRef := ""

	if req != nil {
		organizationRef = strings.TrimSpace(req.GetOrganizationRef())
		gatewayService = strings.TrimSpace(req.GetGatewayService())
		operationRef = strings.TrimSpace(req.GetOperationRef())
	}

	logger := s.logger.With(
		zap.String("organization_ref", organizationRef),
		zap.String("gateway_service", gatewayService),
		zap.String("operation_ref", operationRef),
	)

	defer func() {
		statusLabel := statusFromError(err)
		observeRequest("get_operation_document", "operation", statusLabel, time.Since(start))

		if resp != nil {
			observeDocumentBytes("operation", len(resp.GetContent()))
		}

		contentBytes := 0
		if resp != nil {
			contentBytes = len(resp.GetContent())
		}

		fields := []zap.Field{
			zap.String("status", statusLabel),
			zap.Duration("duration", time.Since(start)),
			zap.Int("content_bytes", contentBytes),
		}

		if err != nil {
			logger.Warn("GetOperationDocument failed", append(fields, zap.Error(err))...)

			return
		}

		logger.Info("GetOperationDocument finished", fields...)
	}()

	if req == nil {
		err = status.Error(codes.InvalidArgument, "request is required")

		return nil, err
	}

	if organizationRef == "" {
		err = status.Error(codes.InvalidArgument, "organization_ref is required")

		return nil, err
	}

	if gatewayService == "" {
		err = status.Error(codes.InvalidArgument, "gateway_service is required")

		return nil, err
	}

	if operationRef == "" {
		err = status.Error(codes.InvalidArgument, "operation_ref is required")

		return nil, err
	}

	snapshot := operationSnapshotFromRequest(req)
	content, _, genErr := s.generateOperationPDF(snapshot)
	if genErr != nil {
		err = status.Error(codes.Internal, genErr.Error())

		return nil, err
	}

	resp = &documentsv1.GetDocumentResponse{
		Content:  content,
		Filename: operationDocumentFilename(operationRef),
		MimeType: "application/pdf",
	}

	return resp, nil
}

func (s *Service) startDiscoveryAnnouncer() {
	if s == nil || s.producer == nil {
		return
	}

	announce := discovery.Announcement{
		Service:    mservice.BillingDocuments,
		Operations: []string{discovery.OperationDocumentsGet},
		InvokeURI:  s.invokeURI,
		Version:    appversion.Create().Short(),
	}
	s.announcer = discovery.NewAnnouncer(s.logger, s.producer, mservice.BillingDocuments, announce)
	s.announcer.Start()
}

func (s *Service) generateActPDF(snapshot model.ActSnapshot) ([]byte, string, error) {
	blocks, err := s.template.Render(snapshot)
	if err != nil {
		return nil, "", err
	}

	return s.renderPDFWithIntegrity(blocks)
}

func (s *Service) generateOperationPDF(snapshot operationSnapshot) ([]byte, string, error) {
	return s.renderPDFWithIntegrity(buildOperationBlocks(snapshot))
}

func (s *Service) renderPDFWithIntegrity(blocks []renderer.Block) ([]byte, string, error) {
	generated := renderer.Renderer{
		Issuer:        s.config.IssuerDetails(),
		OwnerPassword: s.config.Protection.OwnerPassword,
	}

	placeholder := strings.Repeat("0", 64)

	firstPass, err := generated.Render(blocks, placeholder)
	if err != nil {
		return nil, "", err
	}

	footerHash := sha256.Sum256(firstPass)
	footerHex := hex.EncodeToString(footerHash[:])

	finalBytes, err := generated.Render(blocks, footerHex)
	if err != nil {
		return nil, "", err
	}

	return finalBytes, footerHex, nil
}

type operationSnapshot struct {
	OrganizationRef string
	GatewayService  string
	OperationRef    string
	PaymentRef      string
	ClientName      string
	ClientAddress   string
	OperationCode   string
	OperationLabel  string
	OperationState  string
	FailureCode     string
	FailureReason   string
	Amount          string
	Currency        string
	StartedAt       time.Time
	CompletedAt     time.Time
}

func operationSnapshotFromRequest(req *documentsv1.GetOperationDocumentRequest) operationSnapshot {
	snapshot := operationSnapshot{
		OrganizationRef: strings.TrimSpace(req.GetOrganizationRef()),
		GatewayService:  strings.TrimSpace(req.GetGatewayService()),
		OperationRef:    strings.TrimSpace(req.GetOperationRef()),
		PaymentRef:      strings.TrimSpace(req.GetPaymentRef()),
		ClientName:      strings.TrimSpace(req.GetClientName()),
		ClientAddress:   strings.TrimSpace(req.GetClientAddress()),
		OperationCode:   strings.TrimSpace(req.GetOperationCode()),
		OperationLabel:  strings.TrimSpace(req.GetOperationLabel()),
		OperationState:  strings.TrimSpace(req.GetOperationState()),
		FailureCode:     strings.TrimSpace(req.GetFailureCode()),
		FailureReason:   strings.TrimSpace(req.GetFailureReason()),
		Amount:          strings.TrimSpace(req.GetAmount()),
		Currency:        strings.TrimSpace(req.GetCurrency()),
	}

	if ts := req.GetStartedAtUnixMs(); ts > 0 {
		snapshot.StartedAt = time.UnixMilli(ts).UTC()
	}
	if ts := req.GetCompletedAtUnixMs(); ts > 0 {
		snapshot.CompletedAt = time.UnixMilli(ts).UTC()
	}

	return snapshot
}

func buildOperationBlocks(snapshot operationSnapshot) []renderer.Block {
	documentCopy := content.OperationDocument

	blocks := []renderer.Block{
		{
			Tag:   renderer.TagTitle,
			Lines: []string{documentCopy.Title},
		},
		{
			Tag:   renderer.TagSubtitle,
			Lines: []string{documentCopy.Subtitle},
		},
		{
			Tag: renderer.TagMeta,
			Lines: []string{
				fmt.Sprintf("%s: %s", documentCopy.MetaCertificateNumberLabel, certificateNumber(snapshot)),
				fmt.Sprintf("%s: %s", documentCopy.MetaDateLabel, formatCertificateDate(certificateDate(snapshot))),
			},
		},
		{
			Tag:   renderer.TagSection,
			Lines: []string{documentCopy.SectionParties},
		},
		{
			Tag:   renderer.TagText,
			Lines: []string{documentCopy.PartiesIntro},
		},
		{
			Tag: renderer.TagKV,
			Rows: [][]string{
				{documentCopy.RowServiceProvider, content.IssuerLegalName},
				{documentCopy.RowServiceProviderAddress, content.IssuerLegalAddress},
				{documentCopy.RowServiceProviderEmail, content.IssuerEmail},
				{documentCopy.RowClient, certificateClientName(snapshot)},
				{documentCopy.RowClientAddress, certificateClientAddress(snapshot)},
				{documentCopy.RowClientReference, safeValue(snapshot.PaymentRef)},
			},
		},
		{
			Tag:   renderer.TagSection,
			Lines: []string{documentCopy.SectionSubject},
		},
		{
			Tag: renderer.TagText,
			Lines: []string{
				documentCopy.SubjectIntro,
				"",
				"- Payment execution and orchestration services for payment reference " + safeValue(snapshot.PaymentRef) + ".",
				"- Gateway service: " + safeValue(snapshot.GatewayService) + ".",
				"- Operation reference: " + safeValue(snapshot.OperationRef) + ".",
				"- Operation descriptor: " + operationDescriptor(snapshot) + ".",
			},
		},
		{
			Tag:   renderer.TagSection,
			Lines: []string{documentCopy.SectionServicePeriod},
		},
		{
			Tag: renderer.TagKV,
			Rows: [][]string{
				{documentCopy.RowPeriodFrom, formatSnapshotTime(snapshot.StartedAt)},
				{documentCopy.RowPeriodTo, formatSnapshotTime(snapshot.CompletedAt)},
			},
		},
		{
			Tag:   renderer.TagSection,
			Lines: []string{documentCopy.SectionTotalAmount},
		},
		{
			Tag: renderer.TagKV,
			Rows: [][]string{
				{documentCopy.RowTotalAmount, operationAmount(snapshot)},
			},
		},
		{
			Tag:   renderer.TagSection,
			Lines: []string{documentCopy.SectionClientConfirmation},
		},
		{
			Tag: renderer.TagText,
			Lines: []string{
				documentCopy.ConfirmationLine1,
				documentCopy.ConfirmationLine2,
				documentCopy.ConfirmationLine3,
				"",
				"This Certificate serves as confirmation of the completion and acceptance of the services.",
			},
		},
		{
			Tag:   renderer.TagSection,
			Lines: []string{documentCopy.SectionSignatures},
		},
		{
			Tag: renderer.TagSign,
			Lines: []string{
				documentCopy.SignatureServiceProviderLine,
				"",
				documentCopy.SignatureClientNamePrefix + " " + certificateClientName(snapshot),
				documentCopy.SignatureClientTitleLine,
				documentCopy.SignatureClientLine,
			},
		},
		{
			Tag:   renderer.TagSection,
			Lines: []string{documentCopy.SectionOperationStatus},
		},
		{
			Tag: renderer.TagKV,
			Rows: [][]string{
				{documentCopy.RowOperationStatus, safeValue(snapshot.OperationState)},
				{documentCopy.RowOperationCode, safeValue(snapshot.OperationCode)},
				{documentCopy.RowOperationLabel, safeValue(snapshot.OperationLabel)},
			},
		},
	}

	if snapshot.FailureCode != "" || snapshot.FailureReason != "" {
		blocks = append(blocks, renderer.Block{
			Tag: renderer.TagKV,
			Rows: [][]string{
				{documentCopy.RowFailureCode, safeValue(snapshot.FailureCode)},
				{documentCopy.RowFailureReason, safeValue(snapshot.FailureReason)},
			},
		})
	}

	return blocks
}

func formatSnapshotTime(value time.Time) string {
	if value.IsZero() {
		return content.OperationDocument.MissingValuePlaceholder
	}

	return value.UTC().Format(time.RFC3339)
}

func certificateNumber(snapshot operationSnapshot) string {
	if paymentRef := strings.TrimSpace(snapshot.PaymentRef); paymentRef != "" {
		return paymentRef
	}

	if operationRef := strings.TrimSpace(snapshot.OperationRef); operationRef != "" {
		return operationRef
	}

	return content.OperationDocument.MissingValuePlaceholder
}

func certificateDate(snapshot operationSnapshot) time.Time {
	if !snapshot.CompletedAt.IsZero() {
		return snapshot.CompletedAt.UTC()
	}

	if !snapshot.StartedAt.IsZero() {
		return snapshot.StartedAt.UTC()
	}

	return time.Now().UTC()
}

func formatCertificateDate(value time.Time) string {
	if value.IsZero() {
		return content.OperationDocument.MissingValuePlaceholder
	}

	return value.UTC().Format("January 2, 2006")
}

func operationAmount(snapshot operationSnapshot) string {
	amount := strings.TrimSpace(snapshot.Amount)
	currency := strings.TrimSpace(snapshot.Currency)
	if amount == "" && currency == "" {
		return content.OperationDocument.MissingValuePlaceholder
	}

	return strings.TrimSpace(amount + " " + currency)
}

func operationDescriptor(snapshot operationSnapshot) string {
	label := strings.TrimSpace(snapshot.OperationLabel)
	code := strings.TrimSpace(snapshot.OperationCode)

	switch {
	case label != "" && code != "":
		return fmt.Sprintf("%s (%s)", label, code)
	case label != "":
		return label
	case code != "":
		return code
	default:
		return content.OperationDocument.MissingValuePlaceholder
	}
}

func certificateClientName(snapshot operationSnapshot) string {
	if name := strings.TrimSpace(snapshot.ClientName); name != "" {
		return name
	}

	return "John Doe"
}

func certificateClientAddress(snapshot operationSnapshot) string {
	if address := strings.TrimSpace(snapshot.ClientAddress); address != "" {
		return address
	}

	return content.OperationDocument.MissingValuePlaceholder
}

func safeValue(value string) string {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return content.OperationDocument.MissingValuePlaceholder
	}

	return trimmed
}

func operationDocumentFilename(operationRef string) string {
	clean := sanitizeFilenameComponent(operationRef)
	if clean == "" {
		clean = "operation"
	}

	return fmt.Sprintf("operation_%s.pdf", clean)
}

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('_')
		}
	}

	return strings.Trim(b.String(), "_")
}
