// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package esbuild provides functions for building JavaScript resources.
package esbuild

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/evanw/esbuild/pkg/api"
	"github.com/gohugoio/hugo/common/herrors"
	"github.com/gohugoio/hugo/common/hugio"
	"github.com/gohugoio/hugo/common/text"
	"github.com/gohugoio/hugo/hugofs"
	"github.com/gohugoio/hugo/hugolib/filesystems"
	"github.com/gohugoio/hugo/identity"
	"github.com/gohugoio/hugo/resources"
)

// NewBuildClient creates a new BuildClient.
func NewBuildClient(fs *filesystems.SourceFilesystem, rs *resources.Spec, cssMode bool) *BuildClient {
	return &BuildClient{
		rs:      rs,
		sfs:     fs,
		CssMode: cssMode,
	}
}

// BuildClient is a client for building JavaScript resources using esbuild.
type BuildClient struct {
	rs      *resources.Spec
	sfs     *filesystems.SourceFilesystem
	CssMode bool
}

// Spec returns the resources.Spec for this client.
func (c *BuildClient) Spec() *resources.Spec {
	return c.rs
}

// Build builds the given JavaScript resources using esbuild with the given options.
func (c *BuildClient) Build(opts Options) (api.BuildResult, error) {
	dependencyManager := opts.DependencyManager
	if dependencyManager == nil {
		dependencyManager = identity.NopManager
	}

	opts.OutDir = c.rs.AbsPublishDir
	opts.ResolveDir = c.rs.Cfg.BaseConfig().WorkingDir // where node_modules gets resolved
	opts.AbsWorkingDir = opts.ResolveDir
	opts.TsConfig = c.rs.ResolveJSConfigFile("tsconfig.json")
	assetsResolver := newFSResolver(c.rs.Assets.Fs)

	if err := opts.validate(); err != nil {
		return api.BuildResult{}, err
	}

	if err := opts.compile(); err != nil {
		return api.BuildResult{}, err
	}

	var err error
	opts.compiled.Plugins, err = createBuildPlugins(c.rs, assetsResolver, dependencyManager, opts)
	if err != nil {
		return api.BuildResult{}, err
	}

	if opts.Inject != nil {
		// Resolve the absolute filenames.
		for i, ext := range opts.Inject {
			impPath := filepath.FromSlash(ext)
			if filepath.IsAbs(impPath) {
				return api.BuildResult{}, fmt.Errorf("inject: absolute paths not supported, must be relative to /assets")
			}

			m := assetsResolver.resolveComponent(impPath, false)

			if m == nil {
				return api.BuildResult{}, fmt.Errorf("inject: file %q not found", ext)
			}

			opts.Inject[i] = m.Filename

		}

		opts.compiled.Inject = opts.Inject

	}

	result := api.Build(opts.compiled)

	if len(result.Errors) > 0 {
		createErr := func(msg api.Message) error {
			if msg.Location == nil {
				return errors.New(msg.Text)
			}
			var (
				contentr     hugio.ReadSeekCloser
				errorMessage string
				loc          = msg.Location
				errorPath    = loc.File
				err          error
			)

			var resolvedError *ErrorMessageResolved

			if opts.ErrorMessageResolveFunc != nil {
				resolvedError = opts.ErrorMessageResolveFunc(msg)
			}

			if resolvedError == nil {
				if errorPath == stdinImporter {
					errorPath = opts.StdinSourcePath
				}

				errorMessage = msg.Text

				var namespace string
				for _, ns := range hugoNamespaces {
					if strings.HasPrefix(errorPath, ns) {
						namespace = ns
						break
					}
				}

				if namespace != "" {
					namespace += ":"
					errorMessage = strings.ReplaceAll(errorMessage, namespace, "")
					errorPath = strings.TrimPrefix(errorPath, namespace)
					contentr, err = hugofs.Os.Open(errorPath)
				} else {
					var fi os.FileInfo
					fi, err = c.sfs.Fs.Stat(errorPath)
					if err == nil {
						m := fi.(hugofs.FileMetaInfo).Meta()
						errorPath = m.Filename
						contentr, err = m.Open()
					}
				}
			} else {
				contentr = resolvedError.Content
				errorPath = resolvedError.Path
				errorMessage = resolvedError.Message
			}

			if contentr != nil {
				defer contentr.Close()
			}

			if err == nil {
				fe := herrors.
					NewFileErrorFromName(errors.New(errorMessage), errorPath).
					UpdatePosition(text.Position{Offset: -1, LineNumber: loc.Line, ColumnNumber: loc.Column}).
					UpdateContent(contentr, nil)

				return fe
			}

			return fmt.Errorf("%s", errorMessage)
		}

		var errors []error

		for _, msg := range result.Errors {
			errors = append(errors, createErr(msg))
		}

		// Return 1, log the rest.
		for i, err := range errors {
			if i > 0 {
				c.rs.Logger.Errorf("js.Build failed: %s", err)
			}
		}

		return result, errors[0]
	}

	inOutputPathToAbsFilename := opts.ResolveSourceMapSource
	opts.ResolveSourceMapSource = func(s string) string {
		if inOutputPathToAbsFilename != nil {
			if filename := inOutputPathToAbsFilename(s); filename != "" {
				return filename
			}
		}

		if m := assetsResolver.resolveComponent(s, false); m != nil {
			return m.Filename
		}

		return ""
	}

	for i, o := range result.OutputFiles {
		if err := fixOutputFile(&o, func(s string) string {
			if s == "<stdin>" {
				return opts.ResolveSourceMapSource(opts.StdinSourcePath)
			}
			var isNsHugo bool
			if strings.HasPrefix(s, "ns-hugo") {
				isNsHugo = true
				idxColon := strings.Index(s, ":")
				s = s[idxColon+1:]
			}

			if !strings.HasPrefix(s, PrefixHugoVirtual) {
				if !filepath.IsAbs(s) {
					s = filepath.Join(opts.OutDir, s)
				}
			}

			if isNsHugo {
				if ss := opts.ResolveSourceMapSource(s); ss != "" {
					if strings.HasPrefix(ss, PrefixHugoMemory) {
						// File not on disk, mark it for removal from the sources slice.
						return ""
					}
					return ss
				}
				if strings.HasPrefix(s, opts.OutDir) {
					// This is an output file, not a source file.
					return ""
				}
				// s is already the absolute filename set by the Hugo resolve plugin.
				return s
			}
			return s
		}); err != nil {
			return result, err
		}
		result.OutputFiles[i] = o
	}

	return result, nil
}
