package api

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"os"
	"os/exec"
	"strings"
	"syscall"
	"time"

	"github.com/alexliesenfeld/health"
	flag "github.com/spf13/pflag"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
	"go.opentelemetry.io/otel/metric"
	semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
	"go.opentelemetry.io/otel/trace"

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

func init() {
	gotenberg.MustRegisterModule(new(Api))
}

var (
	// ErrInvalidPdfFormats happens if LibreOffice cannot handle the PDF
	// formats option.
	ErrInvalidPdfFormats = errors.New("invalid PDF formats")

	// ErrUnoException happens when unoconverter returns exit code 5.
	ErrUnoException = errors.New("uno exception")

	// ErrRuntimeException happens when unoconverter returns exit code 6.
	ErrRuntimeException = errors.New("uno exception")

	// ErrCoreDumped happens randomly; sometimes a conversion will work as
	// expected, and some other time the same conversion will fail.
	// See https://github.com/gotenberg/gotenberg/issues/639.
	ErrCoreDumped = errors.New("core dumped")
)

// Api is a module that provides a [Uno] to interact with LibreOffice.
type Api struct {
	autoStart bool
	args      libreOfficeArguments

	logger      *slog.Logger
	libreOffice libreOffice
	supervisor  gotenberg.ProcessSupervisor

	reqsCounter               metric.Int64Counter
	errsCounter               metric.Int64Counter
	conversionDurationCounter metric.Float64Histogram
	queueWaitDurationCounter  metric.Float64Histogram
	pdfOutputSizeCounter      metric.Int64Histogram
}

// Options gathers available options when converting a document to PDF.
// See: https://help.libreoffice.org/latest/en-US/text/shared/guide/pdf_params.html.
type Options struct {
	// Password specifies the password for opening the source file.
	Password string // #nosec

	// Landscape allows changing the orientation of the resulting PDF.
	Landscape bool

	// PageRanges allows selecting the pages to convert.
	PageRanges string

	// UpdateIndexes specifies whether to update the indexes before conversion,
	// keeping in mind that doing so might result in missing links in the final
	// PDF.
	UpdateIndexes bool

	// ExportFormFields specifies whether form fields are exported as widgets
	// or only their fixed print representation is exported.
	ExportFormFields bool

	// AllowDuplicateFieldNames specifies whether multiple form fields exported
	// are allowed to have the same field name.
	AllowDuplicateFieldNames bool

	// ExportBookmarks specifies if bookmarks are exported to PDF.
	ExportBookmarks bool

	// ExportBookmarksToPdfDestination specifies that the bookmarks contained
	// in the source LibreOffice file should be exported to the PDF file as
	// Named Destination.
	ExportBookmarksToPdfDestination bool

	// ExportPlaceholders exports the placeholder fields visual markings only.
	// The exported placeholder is ineffective.
	ExportPlaceholders bool

	// ExportNotes specifies if notes are exported to PDF.
	ExportNotes bool

	// ExportNotesPages specifies if notes pages are exported to PDF.
	// Notes pages are available in Impress documents only.
	ExportNotesPages bool

	// ExportOnlyNotesPages specifies if the property ExportNotesPages is set
	// to true if only notes pages are exported to PDF.
	ExportOnlyNotesPages bool

	// ExportNotesInMargin specifies if notes in the margin are exported to
	// PDF.
	ExportNotesInMargin bool

	// ConvertOooTargetToPdfTarget specifies that the target documents with
	// .od[tpgs] extension will have that extension changed to .pdf when the
	// link is exported to PDF. The source document remains untouched.
	ConvertOooTargetToPdfTarget bool

	// ExportLinksRelativeFsys specifies that the file system related
	// hyperlinks (file:// protocol) present in the document will be exported
	// as relative to the source document location.
	ExportLinksRelativeFsys bool

	// ExportHiddenSlides exports, for LibreOffice Impress, slides that are not
	// included in slide shows.
	ExportHiddenSlides bool

	// SkipEmptyPages specifies that automatically inserted empty pages are
	// suppressed. This option is active only if storing Writer documents.
	SkipEmptyPages bool

	// AddOriginalDocumentAsStream specifies that a stream is inserted to the
	// PDF file which contains the original document for archiving purposes.
	AddOriginalDocumentAsStream bool

	// SinglePageSheets ignores each sheet’s paper size, print ranges and
	// shown/hidden status and puts every sheet (even hidden sheets) on exactly
	// one page.
	SinglePageSheets bool

	// InitialView specifies how the PDF document should be displayed when
	// opened. 0 = neither outlines nor thumbnails, 1 = outline pane open,
	// 2 = thumbnail pane open.
	InitialView int

	// InitialPage specifies the page on which the PDF document should be
	// opened in the viewer.
	InitialPage int

	// Magnification specifies the action to be performed when the PDF document
	// is opened. 0 = default, 1 = fit entire page, 2 = fit page width,
	// 3 = fit visible, 4 = use zoom value from Zoom property.
	Magnification int

	// Zoom specifies the zoom level the PDF document is opened with. Only
	// used if Magnification is set to 4.
	Zoom int

	// PageLayout specifies the page layout when the document is opened.
	// 0 = default, 1 = single page, 2 = one column, 3 = two columns.
	PageLayout int

	// FirstPageOnLeft is used with PageLayout value 3. If true, the first
	// page is displayed on the left side.
	FirstPageOnLeft bool

	// ResizeWindowToInitialPage specifies that the PDF viewer window is
	// resized to show the whole initial page.
	ResizeWindowToInitialPage bool

	// CenterWindow specifies that the PDF viewer window is centered on the
	// screen.
	CenterWindow bool

	// OpenInFullScreenMode specifies that the PDF viewer window is opened
	// full screen.
	OpenInFullScreenMode bool

	// DisplayPDFDocumentTitle specifies that the document title is displayed
	// in the PDF viewer title bar.
	DisplayPDFDocumentTitle bool

	// HideViewerMenubar specifies whether to hide the PDF viewer menubar.
	HideViewerMenubar bool

	// HideViewerToolbar specifies whether to hide the PDF viewer toolbar.
	HideViewerToolbar bool

	// HideViewerWindowControls specifies whether to hide the PDF viewer
	// window controls.
	HideViewerWindowControls bool

	// UseTransitionEffects specifies that slide transitions are exported to
	// PDF. Only active for Impress documents.
	UseTransitionEffects bool

	// OpenBookmarkLevels specifies how many bookmark levels should be opened
	// in the reader. -1 = all levels, 1-10 = specific level.
	OpenBookmarkLevels int

	// LosslessImageCompression specifies if images are exported to PDF using
	// a lossless compression format like PNG or compressed using the JPEG
	// format.
	LosslessImageCompression bool

	// Quality specifies the quality of the JPG export. A higher value produces
	// a higher-quality image and a larger file. Between 1 and 100.
	Quality int

	// ReduceImageResolution specifies if the resolution of each image is
	// reduced to the resolution specified by the property MaxImageResolution.
	ReduceImageResolution bool

	// MaxImageResolution, if the property ReduceImageResolution is set to
	// true, tells if all images will be reduced to the given value in DPI.
	// Possible values are: 75, 150, 300, 600 and 1200.
	MaxImageResolution int

	// NativeWatermarkText specifies the text for a watermark to be drawn on
	// every page of the exported PDF file.
	// See https://help.libreoffice.org/latest/en-US/text/shared/guide/pdf_params.html.
	NativeWatermarkText string

	// NativeWatermarkColor specifies the color for the watermark text as a
	// decimal long value. Default is 8388223 (light green).
	NativeWatermarkColor int

	// NativeWatermarkFontHeight specifies the font size for the watermark text.
	NativeWatermarkFontHeight int

	// NativeWatermarkRotateAngle specifies the rotation angle for the watermark
	// text in tenths of a degree (e.g., 450 = 45°).
	NativeWatermarkRotateAngle int

	// NativeWatermarkFontName specifies the font name for the watermark text.
	// Default is "Helvetica".
	NativeWatermarkFontName string

	// NativeTiledWatermarkText specifies the tiled watermark text.
	NativeTiledWatermarkText string

	// PdfFormats allows to convert the resulting PDF to PDF/A-1b, PDF/A-2b,
	// PDF/A-3b and PDF/UA.
	PdfFormats gotenberg.PdfFormats
}

// DefaultOptions returns the default values for Options.
func DefaultOptions() Options {
	return Options{
		Password:                        "",
		Landscape:                       false,
		PageRanges:                      "",
		UpdateIndexes:                   true,
		ExportFormFields:                true,
		AllowDuplicateFieldNames:        false,
		ExportBookmarks:                 true,
		ExportBookmarksToPdfDestination: false,
		ExportPlaceholders:              false,
		ExportNotes:                     false,
		ExportNotesPages:                false,
		ExportOnlyNotesPages:            false,
		ExportNotesInMargin:             false,
		ConvertOooTargetToPdfTarget:     false,
		ExportLinksRelativeFsys:         false,
		ExportHiddenSlides:              false,
		SkipEmptyPages:                  false,
		AddOriginalDocumentAsStream:     false,
		SinglePageSheets:                false,
		InitialView:                     0,
		InitialPage:                     1,
		Magnification:                   0,
		Zoom:                            100,
		PageLayout:                      0,
		FirstPageOnLeft:                 false,
		ResizeWindowToInitialPage:       false,
		CenterWindow:                    false,
		OpenInFullScreenMode:            false,
		DisplayPDFDocumentTitle:         true,
		HideViewerMenubar:               false,
		HideViewerToolbar:               false,
		HideViewerWindowControls:        false,
		UseTransitionEffects:            true,
		OpenBookmarkLevels:              -1,
		LosslessImageCompression:        false,
		Quality:                         90,
		ReduceImageResolution:           false,
		MaxImageResolution:              300,
		NativeWatermarkText:             "",
		NativeWatermarkColor:            8388223,
		NativeWatermarkFontHeight:       0,
		NativeWatermarkRotateAngle:      0,
		NativeWatermarkFontName:         "Helvetica",
		NativeTiledWatermarkText:        "",
		PdfFormats: gotenberg.PdfFormats{
			PdfA:  "",
			PdfUa: false,
		},
	}
}

// Uno is an abstraction on top of the Universal Network Objects API.
type Uno interface {
	Pdf(ctx context.Context, logger *slog.Logger, inputPath, outputPath string, options Options) error
	Extensions() []string
}

// Provider is a module interface that exposes a method for creating a
// [Uno] for other modules.
//
//	func (m *YourModule) Provision(ctx *gotenberg.Context) error {
//		provider, _ := ctx.Module(new(libreofficeapi.Provider))
//		libreOffice, _      := provider.(api.Provider).LibreOffice()
//	}
type Provider interface {
	LibreOffice() (Uno, error)
}

// Descriptor returns a [Api]'s module descriptor.
func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
	return gotenberg.ModuleDescriptor{
		ID: "libreoffice-api",
		FlagSet: func() *flag.FlagSet {
			fs := flag.NewFlagSet("api", flag.ExitOnError)
			fs.Int64("libreoffice-restart-after", 10, "Number of conversions after which LibreOffice will automatically restart. Set to 0 to disable this feature")
			fs.Int64("libreoffice-max-queue-size", 0, "Maximum request queue size for LibreOffice. Set to 0 to disable this feature")
			fs.Duration("libreoffice-idle-shutdown-timeout", 0, "Shutdown LibreOffice after being idle for the given duration. Set to 0 to disable this feature")
			fs.Bool("libreoffice-auto-start", false, "Automatically launch LibreOffice upon initialization if set to true; otherwise, LibreOffice will start at the time of the first conversion")
			fs.Duration("libreoffice-start-timeout", time.Duration(20)*time.Second, "Maximum duration to wait for LibreOffice to start or restart")
			fs.StringSlice("libreoffice-allow-list", []string{}, "Set the allowed URLs for LibreOffice outbound fetches (embedded images, linked content) using regular expressions - supports multiple values")
			fs.StringSlice("libreoffice-deny-list", []string{}, "Set the denied URLs for LibreOffice outbound fetches using regular expressions - supports multiple values")
			fs.Bool("libreoffice-deny-private-ips", false, "Reject LibreOffice outbound URLs whose host resolves to a non-public IP address (loopback, RFC1918, link-local, unique-local). Enable on deployments that accept untrusted documents to mitigate SSRF against internal services")
			fs.Bool("libreoffice-deny-public-ips", false, "Reject LibreOffice outbound URLs whose host resolves to a public IP address. Enable on air-gapped or data-governed deployments to prevent outbound traffic from leaving a private network")

			return fs
		}(),
		New: func() gotenberg.Module { return new(Api) },
	}
}

// Provision sets the module properties.
func (a *Api) Provision(ctx *gotenberg.Context) error {
	flags := ctx.ParsedFlags()
	a.autoStart = flags.MustBool("libreoffice-auto-start")

	libreOfficeBinPath, ok := os.LookupEnv("LIBREOFFICE_BIN_PATH")
	if !ok {
		return errors.New("LIBREOFFICE_BIN_PATH environment variable is not set")
	}

	unoBinPath, ok := os.LookupEnv("UNOCONVERTER_BIN_PATH")
	if !ok {
		return errors.New("UNOCONVERTER_BIN_PATH environment variable is not set")
	}

	a.args = libreOfficeArguments{
		binPath:      libreOfficeBinPath,
		unoBinPath:   unoBinPath,
		startTimeout: flags.MustDuration("libreoffice-start-timeout"),
		proxyOptions: outboundProxyOptions{
			allowList:      flags.MustRegexpSlice("libreoffice-allow-list"),
			denyList:       flags.MustRegexpSlice("libreoffice-deny-list"),
			denyPrivateIPs: flags.MustBool("libreoffice-deny-private-ips"),
			denyPublicIPs:  flags.MustBool("libreoffice-deny-public-ips"),
		},
	}

	// Logger.
	a.logger = gotenberg.Logger(a).With(slog.String("logger", "libreoffice"))

	// Process.
	a.libreOffice = newLibreOfficeProcess(a.args)
	a.supervisor = gotenberg.NewProcessSupervisor(a.logger, a.libreOffice, flags.MustInt64("libreoffice-restart-after"), flags.MustInt64("libreoffice-max-queue-size"), 1, flags.MustDuration("libreoffice-idle-shutdown-timeout"))

	// Metrics.
	meter := gotenberg.Meter()

	// Observable gauges.
	var err error

	_, err = meter.Int64ObservableGauge(
		"libreoffice.requests.active",
		metric.WithDescription("Current number of active LibreOffice requests"),
		metric.WithUnit("{request}"),
		metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
			o.Observe(a.supervisor.ActiveTasksCount())
			return nil
		}),
	)
	if err != nil {
		return fmt.Errorf("create libreoffice.requests.active gauge: %w", err)
	}

	_, err = meter.Int64ObservableGauge(
		"libreoffice.requests.queue_size",
		metric.WithDescription("Current number of LibreOffice conversion requests waiting to be treated"),
		metric.WithUnit("{request}"),
		metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
			o.Observe(a.supervisor.ReqQueueSize())
			return nil
		}),
	)
	if err != nil {
		return fmt.Errorf("create libreoffice.requests.queue_size gauge: %w", err)
	}

	_, err = meter.Int64ObservableCounter(
		"libreoffice.process.restarts.total",
		metric.WithDescription("Current number of LibreOffice restarts"),
		metric.WithUnit("{restart}"),
		metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
			o.Observe(a.supervisor.RestartsCount())
			return nil
		}),
	)
	if err != nil {
		return fmt.Errorf("create libreoffice.process.restarts.total counter: %w", err)
	}

	// Counters.
	a.reqsCounter, err = meter.Int64Counter(
		"libreoffice.requests.total",
		metric.WithDescription("Total number of LibreOffice conversion requests"),
		metric.WithUnit("{request}"),
	)
	if err != nil {
		return fmt.Errorf("create libreoffice.requests.total counter: %w", err)
	}

	a.errsCounter, err = meter.Int64Counter(
		"libreoffice.errors.total",
		metric.WithDescription("Total number of LibreOffice conversion errors"),
		metric.WithUnit("{error}"),
	)
	if err != nil {
		return fmt.Errorf("create libreoffice.errors.total counter: %w", err)
	}

	// Histograms.
	durationBuckets := metric.WithExplicitBucketBoundaries(0.5, 1, 2, 5, 10, 30, 60)

	a.conversionDurationCounter, err = meter.Float64Histogram(
		"libreoffice.conversion.duration",
		metric.WithDescription("Duration of LibreOffice conversions"),
		metric.WithUnit("s"),
		durationBuckets,
	)
	if err != nil {
		return fmt.Errorf("create libreoffice.conversion.duration histogram: %w", err)
	}

	a.queueWaitDurationCounter, err = meter.Float64Histogram(
		"libreoffice.queue.wait.duration",
		metric.WithDescription("Duration of waiting in queue for LibreOffice conversions"),
		metric.WithUnit("s"),
		durationBuckets,
	)
	if err != nil {
		return fmt.Errorf("create libreoffice.queue.wait.duration histogram: %w", err)
	}

	a.pdfOutputSizeCounter, err = meter.Int64Histogram(
		"libreoffice.pdf.output.size",
		metric.WithDescription("Size of PDF output from LibreOffice conversions"),
		metric.WithUnit("By"),
	)
	if err != nil {
		return fmt.Errorf("create libreoffice.pdf.output.size histogram: %w", err)
	}

	return nil
}

// Validate validates the module properties.
func (a *Api) Validate() error {
	var err error

	_, statErr := os.Stat(a.args.binPath)
	if os.IsNotExist(statErr) {
		err = errors.Join(err, fmt.Errorf("LibreOffice binary path does not exist: %w", statErr))
	}

	_, statErr = os.Stat(a.args.unoBinPath)
	if os.IsNotExist(statErr) {
		err = errors.Join(err, fmt.Errorf("unoconverter binary path does not exist: %w", statErr))
	}

	return err
}

// Start does nothing if auto-start is not enabled. Otherwise, it starts a
// LibreOffice instance.
func (a *Api) Start() error {
	if !a.autoStart {
		return nil
	}

	err := a.supervisor.Launch()
	if err != nil {
		return fmt.Errorf("launch supervisor: %w", err)
	}

	return nil
}

// StartupMessage returns a custom startup message.
func (a *Api) StartupMessage() string {
	if !a.autoStart {
		return "LibreOffice ready to start"
	}

	return "LibreOffice automatically started"
}

// Stop stops the current browser instance.
func (a *Api) Stop(ctx context.Context) error {
	// Block until the context is done so that another module may gracefully
	// stop before we do a shutdown.
	a.logger.DebugContext(ctx, "wait for the end of grace duration")

	<-ctx.Done()

	err := a.supervisor.Shutdown()
	if err == nil {
		return nil
	}

	return fmt.Errorf("stop LibreOffice: %w", err)
}

// Debug returns additional debug data.
func (a *Api) Debug() map[string]any {
	debug := make(map[string]any)

	cmd := exec.Command(a.args.binPath, "--version") //nolint:gosec
	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

	output, err := cmd.Output()
	if err != nil {
		debug["version"] = err.Error()
		return debug
	}

	debug["version"] = strings.TrimSpace(string(output))
	return debug
}

// Metrics returns the metrics.
func (a *Api) Metrics() ([]gotenberg.Metric, error) {
	return []gotenberg.Metric{
		{
			Name:        "libreoffice_requests_queue_size",
			Description: "Current number of LibreOffice conversion requests waiting to be treated.",
			Read: func() float64 {
				return float64(a.supervisor.ReqQueueSize())
			},
		},
		{
			Name:        "libreoffice_restarts_count",
			Description: "Current number of LibreOffice restarts.",
			Read: func() float64 {
				return float64(a.supervisor.RestartsCount())
			},
		},
	}, nil
}

// Checks adds a health check that verifies if LibreOffice is healthy.
func (a *Api) Checks() ([]health.CheckerOption, error) {
	return []health.CheckerOption{
		health.WithCheck(health.Check{
			Name: "libreoffice",
			Check: func(_ context.Context) error {
				if a.supervisor.Healthy() {
					return nil
				}

				return errors.New("LibreOffice is unhealthy")
			},
		}),
	}, nil
}

// Ready returns no error if the module is ready.
func (a *Api) Ready() error {
	if !a.autoStart {
		return nil
	}

	ctx, cancel := context.WithTimeout(context.Background(), a.args.startTimeout)
	defer cancel()

	ticker := time.NewTicker(time.Duration(100) * time.Millisecond)

	for {
		select {
		case <-ctx.Done():
			ticker.Stop()
			return fmt.Errorf("context done while waiting for LibreOffice to be ready: %w", ctx.Err())
		case <-ticker.C:
			ok := a.libreOffice.Healthy(a.logger)
			if ok {
				ticker.Stop()
				return nil
			}

			continue
		}
	}
}

// LibreOffice returns a [Uno] for interacting with LibreOffice.
func (a *Api) LibreOffice() (Uno, error) {
	return a, nil
}

// Pdf converts a document to PDF.
func (a *Api) Pdf(ctx context.Context, logger *slog.Logger, inputPath, outputPath string, options Options) error {
	ctx, span := gotenberg.Tracer().Start(ctx, "libreoffice.Pdf",
		trace.WithSpanKind(trace.SpanKindClient),
		trace.WithAttributes(semconv.ServerAddress(a.args.binPath)),
	)
	defer span.End()

	start := time.Now()
	var conversionStart time.Time

	err := a.supervisor.Run(ctx, logger, func() error {
		conversionStart = time.Now()
		return a.libreOffice.pdf(ctx, logger, inputPath, outputPath, options)
	})

	// Determine status and error reason.
	status := "success"
	reason := ""

	if err != nil {
		switch {
		case errors.Is(err, context.DeadlineExceeded):
			status = "timeout"
			reason = "timeout"
		case errors.Is(err, context.Canceled):
			status = "error"
			reason = "context_cancelled"
		case errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded) || errors.Is(err, gotenberg.ErrProcessAlreadyRestarting):
			status = "error"
			reason = "libreoffice_unavailable"
		default:
			status = "error"
			reason = "unknown"
		}
	}

	// Record metrics.
	attrs := metric.WithAttributes(attribute.String("status", status))
	a.reqsCounter.Add(ctx, 1, attrs)

	if reason != "" {
		a.errsCounter.Add(ctx, 1, metric.WithAttributes(attribute.String("reason", reason)))
	}

	if !conversionStart.IsZero() {
		queueWait := conversionStart.Sub(start).Seconds()
		a.queueWaitDurationCounter.Record(ctx, queueWait, attrs)

		conversionDuration := time.Since(conversionStart).Seconds()
		a.conversionDurationCounter.Record(ctx, conversionDuration, attrs)
	}

	if err == nil {
		stat, statErr := os.Stat(outputPath)
		if statErr == nil {
			a.pdfOutputSizeCounter.Record(ctx, stat.Size(), attrs)
		}

		span.SetStatus(codes.Ok, "")
		return nil
	}

	// See https://github.com/gotenberg/gotenberg/issues/639.
	if errors.Is(err, ErrCoreDumped) {
		logger.DebugContext(ctx, fmt.Sprintf("got a '%s' error, retry conversion", err))
		return a.Pdf(ctx, logger, inputPath, outputPath, options)
	}

	span.RecordError(err)
	span.SetStatus(codes.Error, err.Error())
	return fmt.Errorf("supervisor run task: %w", err)
}

// Extensions returns the file extensions available for conversions.
// FIXME: don't care, take all on the route level?
func (a *Api) Extensions() []string {
	return []string{
		".123",
		".602",
		".abw",
		".bib",
		".bmp",
		".cdr",
		".cgm",
		".cmx",
		".csv",
		".cwk",
		".dbf",
		".dif",
		".doc",
		".docm",
		".docx",
		".dot",
		".dotm",
		".dotx",
		".dxf",
		".emf",
		".eps",
		".epub",
		".fodg",
		".fodp",
		".fods",
		".fodt",
		".fopd",
		".gif",
		".htm",
		".html",
		".hwp",
		".jpeg",
		".jpg",
		".key",
		".ltx",
		".lwp",
		".mcw",
		".met",
		".mml",
		".mw",
		".numbers",
		".odd",
		".odg",
		".odm",
		".odp",
		".ods",
		".odt",
		".otg",
		".oth",
		".otp",
		".ots",
		".ott",
		".pages",
		".pbm",
		".pcd",
		".pct",
		".pcx",
		".pdb",
		".pdf",
		".pgm",
		".png",
		".pot",
		".potm",
		".potx",
		".ppm",
		".pps",
		".ppt",
		".pptm",
		".pptx",
		".psd",
		".psw",
		".pub",
		".pwp",
		".pxl",
		".ras",
		".rtf",
		".sda",
		".sdc",
		".sdd",
		".sdp",
		".sdw",
		".sgl",
		".slk",
		".smf",
		".stc",
		".std",
		".sti",
		".stw",
		".svg",
		".svm",
		".swf",
		".sxc",
		".sxd",
		".sxg",
		".sxi",
		".sxm",
		".sxw",
		".tga",
		".tif",
		".tiff",
		".txt",
		".uof",
		".uop",
		".uos",
		".uot",
		".vdx",
		".vor",
		".vsd",
		".vsdm",
		".vsdx",
		".wb2",
		".wk1",
		".wks",
		".wmf",
		".wpd",
		".wpg",
		".wps",
		".xbm",
		".xhtml",
		".xls",
		".xlsb",
		".xlsm",
		".xlsx",
		".xlt",
		".xltm",
		".xltx",
		".xlw",
		".xml",
		".xpm",
		".zabw",
	}
}

// Interface guards.
var (
	_ gotenberg.Module          = (*Api)(nil)
	_ gotenberg.Provisioner     = (*Api)(nil)
	_ gotenberg.Validator       = (*Api)(nil)
	_ gotenberg.App             = (*Api)(nil)
	_ gotenberg.Debuggable      = (*Api)(nil)
	_ gotenberg.MetricsProvider = (*Api)(nil)
	_ api.HealthChecker         = (*Api)(nil)
	_ Uno                       = (*Api)(nil)
	_ Provider                  = (*Api)(nil)
)
