package documents

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

	"github.com/tech/sendico/billing/documents/internal/appversion"
	"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 {
	logger    mlogger.Logger
	storage   storage.Repository
	docStore  docstore.Store
	producer  msg.Producer
	announcer *discovery.Announcer
	invokeURI string
	config    Config
	template  TemplateRenderer
	documentsv1.UnimplementedDocumentServiceServer
}

// 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) startDiscoveryAnnouncer() {
	if s == nil || s.producer == nil {
		return
	}
	announce := discovery.Announcement{
		Service:    "BILLING_DOCUMENTS",
		Operations: []string{"documents.batch_resolve", "documents.get"},
		InvokeURI:  s.invokeURI,
		Version:    appversion.Create().Short(),
	}
	s.announcer = discovery.NewAnnouncer(s.logger, s.producer, string(mservice.BillingDocuments), announce)
	s.announcer.Start()
}

func (s *Service) BatchResolveDocuments(ctx context.Context, req *documentsv1.BatchResolveDocumentsRequest) (resp *documentsv1.BatchResolveDocumentsResponse, err error) {
	start := time.Now()
	var paymentRefs []string
	if req != nil {
		paymentRefs = req.GetPaymentRefs()
	}
	logger := s.logger.With(zap.Int("payment_refs", len(paymentRefs)))
	defer func() {
		statusLabel := statusFromError(err)
		observeRequest("batch_resolve", documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED, statusLabel, time.Since(start))
		observeBatchSize(len(paymentRefs))

		itemsCount := 0
		if resp != nil {
			itemsCount = len(resp.GetItems())
		}
		fields := []zap.Field{
			zap.String("status", statusLabel),
			zap.Duration("duration", time.Since(start)),
			zap.Int("items", itemsCount),
		}
		if err != nil {
			logger.Warn("BatchResolveDocuments failed", append(fields, zap.Error(err))...)
			return
		}
		logger.Info("BatchResolveDocuments finished", fields...)
	}()

	if len(paymentRefs) == 0 {
		resp = &documentsv1.BatchResolveDocumentsResponse{}
		return resp, nil
	}

	if s.storage == nil {
		err = status.Error(codes.Unavailable, errStorageUnavailable.Error())
		return nil, err
	}

	refs := make([]string, 0, len(paymentRefs))
	for _, ref := range paymentRefs {
		clean := strings.TrimSpace(ref)
		if clean == "" {
			continue
		}
		refs = append(refs, clean)
	}
	if len(refs) == 0 {
		resp = &documentsv1.BatchResolveDocumentsResponse{}
		return resp, nil
	}

	records, err := s.storage.Documents().ListByPaymentRefs(ctx, refs)
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	recordByRef := map[string]*model.DocumentRecord{}
	for _, record := range records {
		if record == nil {
			continue
		}
		recordByRef[record.PaymentRef] = record
	}

	items := make([]*documentsv1.DocumentMeta, 0, len(refs))
	for _, ref := range refs {
		meta := &documentsv1.DocumentMeta{PaymentRef: ref}
		if record := recordByRef[ref]; record != nil {
			record.Normalize()
			available := []model.DocumentType{model.DocumentTypeAct}
			ready := make([]model.DocumentType, 0, 1)
			if path, ok := record.StoragePaths[model.DocumentTypeAct]; ok && path != "" {
				ready = append(ready, model.DocumentTypeAct)
			}
			meta.AvailableTypes = toProtoTypes(available)
			meta.ReadyTypes = toProtoTypes(ready)
		}
		items = append(items, meta)
	}

	resp = &documentsv1.BatchResolveDocumentsResponse{Items: items}
	return resp, nil
}

func (s *Service) GetDocument(ctx context.Context, req *documentsv1.GetDocumentRequest) (resp *documentsv1.GetDocumentResponse, err error) {
	start := time.Now()
	docType := documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED
	paymentRef := ""
	if req != nil {
		docType = req.GetType()
		paymentRef = strings.TrimSpace(req.GetPaymentRef())
	}
	logger := s.logger.With(
		zap.String("payment_ref", paymentRef),
		zap.String("document_type", docTypeLabel(docType)),
	)

	defer func() {
		statusLabel := statusFromError(err)
		observeRequest("get_document", docType, statusLabel, time.Since(start))
		if resp != nil {
			observeDocumentBytes(docType, 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("GetDocument failed", append(fields, zap.Error(err))...)
			return
		}
		logger.Info("GetDocument finished", fields...)
	}()

	if paymentRef == "" {
		err = status.Error(codes.InvalidArgument, "payment_ref is required")
		return nil, err
	}
	if docType == documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED {
		err = status.Error(codes.InvalidArgument, "document type is required")
		return nil, err
	}
	if s.storage == nil {
		err = status.Error(codes.Unavailable, errStorageUnavailable.Error())
		return nil, err
	}
	if s.docStore == nil {
		err = status.Error(codes.Unavailable, errDocStoreUnavailable.Error())
		return nil, err
	}
	if s.template == nil {
		err = status.Error(codes.FailedPrecondition, errTemplateUnavailable.Error())
		return nil, err
	}

	record, err := s.storage.Documents().GetByPaymentRef(ctx, paymentRef)
	if err != nil {
		if errors.Is(err, storage.ErrDocumentNotFound) {
			return nil, status.Error(codes.NotFound, "document record not found")
		}
		return nil, status.Error(codes.Internal, err.Error())
	}
	record.Normalize()

	targetType := model.DocumentTypeFromProto(docType)

	if docType != documentsv1.DocumentType_DOCUMENT_TYPE_ACT {
		return nil, status.Error(codes.Unimplemented, "document type not implemented")
	}

	if path, ok := record.StoragePaths[targetType]; ok && path != "" {
		content, loadErr := s.docStore.Load(ctx, path)
		if loadErr != nil {
			return nil, status.Error(codes.Internal, loadErr.Error())
		}
		return &documentsv1.GetDocumentResponse{
			Content:  content,
			Filename: documentFilename(docType, paymentRef),
			MimeType: "application/pdf",
		}, nil
	}

	content, hash, genErr := s.generateActPDF(record.Snapshot)
	if genErr != nil {
		logger.Warn("Failed to generate document", zap.Error(genErr))
		return nil, status.Error(codes.Internal, genErr.Error())
	}

	path := documentStoragePath(paymentRef, docType)
	if saveErr := s.docStore.Save(ctx, path, content); saveErr != nil {
		logger.Warn("Failed to store document", zap.Error(saveErr))
		return nil, status.Error(codes.Internal, saveErr.Error())
	}

	record.StoragePaths[targetType] = path
	record.Hashes[targetType] = hash
	if updateErr := s.storage.Documents().Update(ctx, record); updateErr != nil {
		logger.Warn("Failed to update document record", zap.Error(updateErr))
		return nil, status.Error(codes.Internal, updateErr.Error())
	}

	resp = &documentsv1.GetDocumentResponse{
		Content:  content,
		Filename: documentFilename(docType, paymentRef),
		MimeType: "application/pdf",
	}
	return resp, nil
}

type serviceError string

func (e serviceError) Error() string {
	return string(e)
}

var (
	errStorageUnavailable  = serviceError("documents: storage not initialised")
	errDocStoreUnavailable = serviceError("documents: document store not initialised")
	errTemplateUnavailable = serviceError("documents: template renderer not initialised")
)

func (s *Service) generateActPDF(snapshot model.ActSnapshot) ([]byte, string, error) {
	blocks, err := s.template.Render(snapshot)
	if err != nil {
		return nil, "", err
	}
	generated := renderer.Renderer{
		Issuer:        s.config.Issuer,
		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
}

func toProtoTypes(types []model.DocumentType) []documentsv1.DocumentType {
	if len(types) == 0 {
		return nil
	}
	result := make([]documentsv1.DocumentType, 0, len(types))
	for _, t := range types {
		result = append(result, t.Proto())
	}
	return result
}

func documentStoragePath(paymentRef string, docType documentsv1.DocumentType) string {
	suffix := "document.pdf"
	switch docType {
	case documentsv1.DocumentType_DOCUMENT_TYPE_ACT:
		suffix = "act.pdf"
	case documentsv1.DocumentType_DOCUMENT_TYPE_INVOICE:
		suffix = "invoice.pdf"
	case documentsv1.DocumentType_DOCUMENT_TYPE_RECEIPT:
		suffix = "receipt.pdf"
	}
	return filepath.ToSlash(filepath.Join("documents", paymentRef, suffix))
}

func documentFilename(docType documentsv1.DocumentType, paymentRef string) string {
	name := "document"
	switch docType {
	case documentsv1.DocumentType_DOCUMENT_TYPE_ACT:
		name = "act"
	case documentsv1.DocumentType_DOCUMENT_TYPE_INVOICE:
		name = "invoice"
	case documentsv1.DocumentType_DOCUMENT_TYPE_RECEIPT:
		name = "receipt"
	}
	return fmt.Sprintf("%s_%s.pdf", name, paymentRef)
}
