package pdfengines

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"sync"

	"go.opentelemetry.io/otel/codes"
	"go.opentelemetry.io/otel/trace"

	"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
)

type multiPdfEngines struct {
	mergeEngines          []gotenberg.PdfEngine
	splitEngines          []gotenberg.PdfEngine
	flattenEngines        []gotenberg.PdfEngine
	convertEngines        []gotenberg.PdfEngine
	readMetadataEngines   []gotenberg.PdfEngine
	writeMetadataEngines  []gotenberg.PdfEngine
	passwordEngines       []gotenberg.PdfEngine
	embedEngines          []gotenberg.PdfEngine
	embedMetadataEngines  []gotenberg.PdfEngine
	readBookmarksEngines  []gotenberg.PdfEngine
	writeBookmarksEngines []gotenberg.PdfEngine
	watermarkEngines      []gotenberg.PdfEngine
	stampEngines          []gotenberg.PdfEngine
	rotateEngines         []gotenberg.PdfEngine
}

func newMultiPdfEngines(
	mergeEngines,
	splitEngines,
	flattenEngines,
	convertEngines,
	readMetadataEngines,
	writeMetadataEngines,
	passwordEngines,
	embedEngines,
	embedMetadataEngines,
	readBookmarksEngines,
	writeBookmarksEngines,
	watermarkEngines,
	stampEngines,
	rotateEngines []gotenberg.PdfEngine,
) *multiPdfEngines {
	return &multiPdfEngines{
		mergeEngines:          mergeEngines,
		splitEngines:          splitEngines,
		flattenEngines:        flattenEngines,
		convertEngines:        convertEngines,
		readMetadataEngines:   readMetadataEngines,
		writeMetadataEngines:  writeMetadataEngines,
		passwordEngines:       passwordEngines,
		embedEngines:          embedEngines,
		embedMetadataEngines:  embedMetadataEngines,
		readBookmarksEngines:  readBookmarksEngines,
		writeBookmarksEngines: writeBookmarksEngines,
		watermarkEngines:      watermarkEngines,
		stampEngines:          stampEngines,
		rotateEngines:         rotateEngines,
	}
}

// Merge combines multiple PDF files into a single document using the first
// available engine that supports PDF merging.
//
//nolint:dupl
func (multi *multiPdfEngines) Merge(ctx context.Context, logger *slog.Logger, inputPaths []string, outputPath string) error {
	tracer := gotenberg.Tracer()
	ctx, span := tracer.Start(ctx, "pdfengines.Merge", trace.WithSpanKind(trace.SpanKindInternal))
	defer span.End()

	var err error
	errChan := make(chan error, 1)

	for _, engine := range multi.mergeEngines {
		go func(engine gotenberg.PdfEngine) {
			errChan <- engine.Merge(ctx, logger, inputPaths, outputPath)
		}(engine)

		select {
		case mergeErr := <-errChan:
			if mergeErr != nil {
				err = errors.Join(err, mergeErr)
			} else {
				span.SetStatus(codes.Ok, "")
				return nil
			}
		case <-ctx.Done():
			return ctx.Err()
		}
	}

	err = fmt.Errorf("merge PDFs with multi PDF engines: %w", err)
	span.RecordError(err)
	span.SetStatus(codes.Error, err.Error())

	return err
}

type splitResult struct {
	outputPaths []string
	err         error
}

// Split divides the PDF into separate pages using the first available engine
// that supports PDF splitting.
func (multi *multiPdfEngines) Split(ctx context.Context, logger *slog.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
	tracer := gotenberg.Tracer()
	ctx, span := tracer.Start(ctx, "pdfengines.Split", trace.WithSpanKind(trace.SpanKindInternal))
	defer span.End()

	var err error
	var mu sync.Mutex // to safely append errors.

	for _, engine := range multi.splitEngines {
		resultChan := make(chan splitResult, 1)

		go func(engine gotenberg.PdfEngine) {
			outputPaths, err := engine.Split(ctx, logger, mode, inputPath, outputDirPath)
			resultChan <- splitResult{outputPaths: outputPaths, err: err}
		}(engine)

		select {
		case result := <-resultChan:
			if result.err != nil {
				mu.Lock()
				err = errors.Join(err, result.err)
				mu.Unlock()
			} else {
				span.SetStatus(codes.Ok, "")
				return result.outputPaths, nil
			}
		case <-ctx.Done():
			return nil, ctx.Err()
		}
	}

	err = fmt.Errorf("split PDF with multi PDF engines: %w", err)
	span.RecordError(err)
	span.SetStatus(codes.Error, err.Error())

	return nil, err
}

// Flatten merges existing annotation appearances with page content using the
// first available engine that supports flattening.
func (multi *multiPdfEngines) Flatten(ctx context.Context, logger *slog.Logger, inputPath string) error {
	tracer := gotenberg.Tracer()
	ctx, span := tracer.Start(ctx, "pdfengines.Flatten", trace.WithSpanKind(trace.SpanKindInternal))
	defer span.End()

	var err error
	errChan := make(chan error, 1)

	for _, engine := range multi.flattenEngines {
		go func(engine gotenberg.PdfEngine) {
			errChan <- engine.Flatten(ctx, logger, inputPath)
		}(engine)

		select {
		case mergeErr := <-errChan:
			if mergeErr != nil {
				err = errors.Join(err, mergeErr)
			} else {
				span.SetStatus(codes.Ok, "")
				return nil
			}
		case <-ctx.Done():
			return ctx.Err()
		}
	}

	err = fmt.Errorf("flatten PDF with multi PDF engines: %w", err)
	span.RecordError(err)
	span.SetStatus(codes.Error, err.Error())

	return err
}

// Convert transforms the given PDF to a specific PDF format using the first
// available engine that supports PDF conversion.
func (multi *multiPdfEngines) Convert(ctx context.Context, logger *slog.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
	tracer := gotenberg.Tracer()
	ctx, span := tracer.Start(ctx, "pdfengines.Convert", trace.WithSpanKind(trace.SpanKindInternal))
	defer span.End()

	var err error
	errChan := make(chan error, 1)

	for _, engine := range multi.convertEngines {
		go func(engine gotenberg.PdfEngine) {
			errChan <- engine.Convert(ctx, logger, formats, inputPath, outputPath)
		}(engine)

		select {
		case mergeErr := <-errChan:
			if mergeErr != nil {
				err = errors.Join(err, mergeErr)
			} else {
				span.SetStatus(codes.Ok, "")
				return nil
			}
		case <-ctx.Done():
			return ctx.Err()
		}
	}

	err = fmt.Errorf("convert PDF to '%+v' with multi PDF engines: %w", formats, err)
	span.RecordError(err)
	span.SetStatus(codes.Error, err.Error())

	return err
}

type readMetadataResult struct {
	metadata map[string]any
	err      error
}

// ReadMetadata extracts metadata from a PDF file using the first available
// engine that supports metadata reading.
//
//nolint:dupl
func (multi *multiPdfEngines) ReadMetadata(ctx context.Context, logger *slog.Logger, inputPath string) (map[string]any, error) {
	tracer := gotenberg.Tracer()
	ctx, span := tracer.Start(ctx, "pdfengines.ReadMetadata", trace.WithSpanKind(trace.SpanKindInternal))
	defer span.End()

	var err error
	var mu sync.Mutex // to safely append errors.

	for _, engine := range multi.readMetadataEngines {
		resultChan := make(chan readMetadataResult, 1)

		go func(engine gotenberg.PdfEngine) {
			metadata, err := engine.ReadMetadata(ctx, logger, inputPath)
			resultChan <- readMetadataResult{metadata: metadata, err: err}
		}(engine)

		select {
		case result := <-resultChan:
			if result.err != nil {
				mu.Lock()
				err = errors.Join(err, result.err)
				mu.Unlock()
			} else {
				span.SetStatus(codes.Ok, "")
				return result.metadata, nil
			}
		case <-ctx.Done():
			return nil, ctx.Err()
		}
	}

	err = fmt.Errorf("read PDF metadata with multi PDF engines: %w", err)
	span.RecordError(err)
	span.SetStatus(codes.Error, err.Error())

	return nil, err
}

// WriteMetadata embeds metadata into a PDF file using the first available
// engine that supports metadata writing.
func (multi *multiPdfEngines) WriteMetadata(ctx context.Context, logger *slog.Logger, metadata map[string]any, inputPath string) error {
	tracer := gotenberg.Tracer()
	ctx, span := tracer.Start(ctx, "pdfengines.WriteMetadata", trace.WithSpanKind(trace.SpanKindInternal))
	defer span.End()

	var err error
	errChan := make(chan error, 1)

	for _, engine := range multi.writeMetadataEngines {
		go func(engine gotenberg.PdfEngine) {
			errChan <- engine.WriteMetadata(ctx, logger, metadata, inputPath)
		}(engine)

		select {
		case writeMetadataErr := <-errChan:
			if writeMetadataErr != nil {
				err = errors.Join(err, writeMetadataErr)
			} else {
				span.SetStatus(codes.Ok, "")
				return nil
			}
		case <-ctx.Done():
			return ctx.Err()
		}
	}

	err = fmt.Errorf("write PDF metadata with multi PDF engines: %w", err)
	span.RecordError(err)
	span.SetStatus(codes.Error, err.Error())

	return err
}

type pageCountResult struct {
	pageCount int
	err       error
}

// PageCount returns the number of pages in a PDF file using the first available
// engine that supports metadata reading.
func (multi *multiPdfEngines) PageCount(ctx context.Context, logger *slog.Logger, inputPath string) (int, error) {
	tracer := gotenberg.Tracer()
	ctx, span := tracer.Start(ctx, "pdfengines.PageCount", trace.WithSpanKind(trace.SpanKindInternal))
	defer span.End()

	var err error
	var mu sync.Mutex // to safely append errors.

	for _, engine := range multi.readMetadataEngines {
		resultChan := make(chan pageCountResult, 1)

		go func(engine gotenberg.PdfEngine) {
			pageCount, err := engine.PageCount(ctx, logger, inputPath)
			resultChan <- pageCountResult{pageCount: pageCount, err: err}
		}(engine)

		select {
		case result := <-resultChan:
			if result.err != nil {
				mu.Lock()
				err = errors.Join(err, result.err)
				mu.Unlock()
			} else {
				span.SetStatus(codes.Ok, "")
				return result.pageCount, nil
			}
		case <-ctx.Done():
			return 0, ctx.Err()
		}
	}

	err = fmt.Errorf("page count with multi PDF engines: %w", err)
	span.RecordError(err)
	span.SetStatus(codes.Error, err.Error())

	return 0, err
}

type readBookmarksResult struct {
	bookmarks []gotenberg.Bookmark
	err       error
}

// ReadBookmarks reads bookmarks from a PDF file using the first available
// engine that supports bookmarks reading.
//
//nolint:dupl
func (multi *multiPdfEngines) ReadBookmarks(ctx context.Context, logger *slog.Logger, inputPath string) ([]gotenberg.Bookmark, error) {
	tracer := gotenberg.Tracer()
	ctx, span := tracer.Start(ctx, "pdfengines.ReadBookmarks", trace.WithSpanKind(trace.SpanKindInternal))
	defer span.End()

	var err error
	var mu sync.Mutex // to safely append errors.

	for _, engine := range multi.readBookmarksEngines {
		resultChan := make(chan readBookmarksResult, 1)

		go func(engine gotenberg.PdfEngine) {
			bookmarks, err := engine.ReadBookmarks(ctx, logger, inputPath)
			resultChan <- readBookmarksResult{bookmarks: bookmarks, err: err}
		}(engine)

		select {
		case result := <-resultChan:
			if result.err != nil {
				mu.Lock()
				err = errors.Join(err, result.err)
				mu.Unlock()
			} else {
				span.SetStatus(codes.Ok, "")
				return result.bookmarks, nil
			}
		case <-ctx.Done():
			return nil, ctx.Err()
		}
	}

	err = fmt.Errorf("read PDF bookmarks with multi PDF engines: %w", err)
	span.RecordError(err)
	span.SetStatus(codes.Error, err.Error())

	return nil, err
}

// WriteBookmarks adds a document outline (bookmarks) to a PDF file using the
// first available engine that supports bookmarks writing.
func (multi *multiPdfEngines) WriteBookmarks(ctx context.Context, logger *slog.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
	tracer := gotenberg.Tracer()
	ctx, span := tracer.Start(ctx, "pdfengines.WriteBookmarks", trace.WithSpanKind(trace.SpanKindInternal))
	defer span.End()

	var err error
	errChan := make(chan error, 1)

	for _, engine := range multi.writeBookmarksEngines {
		go func(engine gotenberg.PdfEngine) {
			errChan <- engine.WriteBookmarks(ctx, logger, inputPath, bookmarks)
		}(engine)

		select {
		case writeBookmarksErr := <-errChan:
			if writeBookmarksErr != nil {
				err = errors.Join(err, writeBookmarksErr)
			} else {
				span.SetStatus(codes.Ok, "")
				return nil
			}
		case <-ctx.Done():
			return ctx.Err()
		}
	}

	err = fmt.Errorf("write PDF bookmarks with multi PDF engines: %w", err)
	span.RecordError(err)
	span.SetStatus(codes.Error, err.Error())

	return err
}

// Encrypt adds password protection to a PDF file using the first available
// engine that supports password protection.
func (multi *multiPdfEngines) Encrypt(ctx context.Context, logger *slog.Logger, inputPath, userPassword, ownerPassword string) error {
	tracer := gotenberg.Tracer()
	ctx, span := tracer.Start(ctx, "pdfengines.Encrypt", trace.WithSpanKind(trace.SpanKindInternal))
	defer span.End()

	var err error
	errChan := make(chan error, 1)

	for _, engine := range multi.passwordEngines {
		go func(engine gotenberg.PdfEngine) {
			errChan <- engine.Encrypt(ctx, logger, inputPath, userPassword, ownerPassword)
		}(engine)

		select {
		case protectErr := <-errChan:
			if protectErr != nil {
				err = errors.Join(err, protectErr)
			} else {
				span.SetStatus(codes.Ok, "")
				return nil
			}
		case <-ctx.Done():
			return ctx.Err()
		}
	}

	err = fmt.Errorf("encrypt PDF using multi PDF engines: %w", err)
	span.RecordError(err)
	span.SetStatus(codes.Error, err.Error())

	return err
}

// EmbedFiles embeds files into a PDF using the first available
// engine that supports file embedding.
//
//nolint:dupl
func (multi *multiPdfEngines) EmbedFiles(ctx context.Context, logger *slog.Logger, filePaths []string, inputPath string) error {
	tracer := gotenberg.Tracer()
	ctx, span := tracer.Start(ctx, "pdfengines.EmbedFiles", trace.WithSpanKind(trace.SpanKindInternal))
	defer span.End()

	var err error
	errChan := make(chan error, 1)

	for _, engine := range multi.embedEngines {
		go func(engine gotenberg.PdfEngine) {
			errChan <- engine.EmbedFiles(ctx, logger, filePaths, inputPath)
		}(engine)

		select {
		case embedErr := <-errChan:
			if embedErr != nil {
				err = errors.Join(err, embedErr)
			} else {
				span.SetStatus(codes.Ok, "")
				return nil
			}
		case <-ctx.Done():
			return ctx.Err()
		}
	}

	err = fmt.Errorf("embed files into PDF using multi PDF engines: %w", err)
	span.RecordError(err)
	span.SetStatus(codes.Error, err.Error())

	return err
}

// Watermark applies a watermark (behind page content) to a PDF file using the
// first available engine that supports watermarking.
//
//nolint:dupl
func (multi *multiPdfEngines) Watermark(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
	tracer := gotenberg.Tracer()
	ctx, span := tracer.Start(ctx, "pdfengines.Watermark", trace.WithSpanKind(trace.SpanKindInternal))
	defer span.End()

	var err error
	errChan := make(chan error, 1)

	for _, engine := range multi.watermarkEngines {
		go func(engine gotenberg.PdfEngine) {
			errChan <- engine.Watermark(ctx, logger, inputPath, stamp)
		}(engine)

		select {
		case watermarkErr := <-errChan:
			if watermarkErr != nil {
				err = errors.Join(err, watermarkErr)
			} else {
				span.SetStatus(codes.Ok, "")
				return nil
			}
		case <-ctx.Done():
			return ctx.Err()
		}
	}

	err = fmt.Errorf("watermark PDF with multi PDF engines: %w", err)
	span.RecordError(err)
	span.SetStatus(codes.Error, err.Error())

	return err
}

// Stamp applies a stamp (on top of page content) to a PDF file using the
// first available engine that supports stamping.
//
//nolint:dupl
func (multi *multiPdfEngines) Stamp(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
	tracer := gotenberg.Tracer()
	ctx, span := tracer.Start(ctx, "pdfengines.Stamp", trace.WithSpanKind(trace.SpanKindInternal))
	defer span.End()

	var err error
	errChan := make(chan error, 1)

	for _, engine := range multi.stampEngines {
		go func(engine gotenberg.PdfEngine) {
			errChan <- engine.Stamp(ctx, logger, inputPath, stamp)
		}(engine)

		select {
		case stampErr := <-errChan:
			if stampErr != nil {
				err = errors.Join(err, stampErr)
			} else {
				span.SetStatus(codes.Ok, "")
				return nil
			}
		case <-ctx.Done():
			return ctx.Err()
		}
	}

	err = fmt.Errorf("stamp PDF with multi PDF engines: %w", err)
	span.RecordError(err)
	span.SetStatus(codes.Error, err.Error())

	return err
}

// Rotate rotates pages of a PDF file using the first available engine that
// supports rotation.
func (multi *multiPdfEngines) Rotate(ctx context.Context, logger *slog.Logger, inputPath string, angle int, pages string) error {
	tracer := gotenberg.Tracer()
	ctx, span := tracer.Start(ctx, "pdfengines.Rotate", trace.WithSpanKind(trace.SpanKindInternal))
	defer span.End()

	var err error
	errChan := make(chan error, 1)

	for _, engine := range multi.rotateEngines {
		go func(engine gotenberg.PdfEngine) {
			errChan <- engine.Rotate(ctx, logger, inputPath, angle, pages)
		}(engine)

		select {
		case rotateErr := <-errChan:
			if rotateErr != nil {
				err = errors.Join(err, rotateErr)
			} else {
				span.SetStatus(codes.Ok, "")
				return nil
			}
		case <-ctx.Done():
			return ctx.Err()
		}
	}

	err = fmt.Errorf("rotate PDF with multi PDF engines: %w", err)
	span.RecordError(err)
	span.SetStatus(codes.Error, err.Error())

	return err
}

// EmbedFilesMetadata sets metadata on embedded files using the first available
// engine that supports it.
//
//nolint:dupl
func (multi *multiPdfEngines) EmbedFilesMetadata(ctx context.Context, logger *slog.Logger, metadata map[string]map[string]string, inputPath string) error {
	tracer := gotenberg.Tracer()
	ctx, span := tracer.Start(ctx, "pdfengines.EmbedFilesMetadata", trace.WithSpanKind(trace.SpanKindInternal))
	defer span.End()

	var err error
	errChan := make(chan error, 1)

	for _, engine := range multi.embedMetadataEngines {
		go func(engine gotenberg.PdfEngine) {
			errChan <- engine.EmbedFilesMetadata(ctx, logger, metadata, inputPath)
		}(engine)

		select {
		case setErr := <-errChan:
			if setErr != nil {
				err = errors.Join(err, setErr)
			} else {
				span.SetStatus(codes.Ok, "")
				return nil
			}
		case <-ctx.Done():
			return ctx.Err()
		}
	}

	err = fmt.Errorf("set embeds metadata using multi PDF engines: %w", err)
	span.RecordError(err)
	span.SetStatus(codes.Error, err.Error())

	return err
}

// Interface guards.
var (
	_ gotenberg.PdfEngine = (*multiPdfEngines)(nil)
)
