// Copyright 2025 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 deps

import (
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"
	"sync/atomic"

	"github.com/bep/logg"
	"github.com/gohugoio/hugo/cache/dynacache"
	"github.com/gohugoio/hugo/cache/filecache"
	"github.com/gohugoio/hugo/common/hexec"
	"github.com/gohugoio/hugo/common/hmaps"
	"github.com/gohugoio/hugo/common/loggers"
	"github.com/gohugoio/hugo/common/types"
	"github.com/gohugoio/hugo/config"
	"github.com/gohugoio/hugo/config/allconfig"
	"github.com/gohugoio/hugo/config/security"
	"github.com/gohugoio/hugo/helpers"
	"github.com/gohugoio/hugo/hugofs"
	"github.com/gohugoio/hugo/identity"
	"github.com/gohugoio/hugo/internal/js"
	"github.com/gohugoio/hugo/internal/warpc"
	"github.com/gohugoio/hugo/media"
	"github.com/gohugoio/hugo/resources/page"
	"github.com/gohugoio/hugo/resources/postpub"
	"github.com/gohugoio/hugo/tpl/tplimpl"

	"github.com/gohugoio/hugo/metrics"
	"github.com/gohugoio/hugo/resources"
	"github.com/gohugoio/hugo/source"
	"github.com/gohugoio/hugo/tpl"
	"github.com/spf13/afero"
)

// Deps holds dependencies used by many.
// There will be normally only one instance of deps in play
// at a given time, i.e. one per Site built.
type Deps struct {
	// The logger to use.
	Log loggers.Logger `json:"-"`

	ExecHelper *hexec.Exec

	// The file systems to use.
	Fs *hugofs.Fs `json:"-"`

	// The PathSpec to use
	*helpers.PathSpec `json:"-"`

	// The ContentSpec to use
	*helpers.ContentSpec `json:"-"`

	// The SourceSpec to use
	SourceSpec *source.SourceSpec `json:"-"`

	// The Resource Spec to use
	ResourceSpec *resources.Spec

	// The configuration to use
	Conf config.AllProvider `json:"-"`

	// The memory cache to use.
	MemCache *dynacache.Cache

	// The translation func to use
	Translate func(ctx context.Context, translationID string, templateData any) string `json:"-"`

	// The site building.
	Site page.Site

	TemplateStore *tplimpl.TemplateStore

	// Used in tests
	OverloadedTemplateFuncs map[string]any

	TranslationProvider ResourceProvider

	Metrics metrics.Provider

	// BuildStartListeners will be notified before a build starts.
	BuildStartListeners *Listeners[any]

	// BuildEndListeners will be notified after a build finishes.
	BuildEndListeners *Listeners[any]

	// OnChangeListeners will be notified when something changes.
	OnChangeListeners *Listeners[identity.Identity]

	// Resources that gets closed when the build is done or the server shuts down.
	BuildClosers *types.Closers

	// This is common/global for all sites.
	BuildState *BuildState

	// Misc counters.
	Counters *Counters

	// Holds RPC dispatchers for Katex etc.
	// TODO(bep) rethink this re. a plugin setup, but this will have to do for now.
	WasmDispatchers *warpc.Dispatchers

	// The JS batcher client.
	JSBatcherClient js.BatcherClient

	isClosed bool

	*globalErrHandler
}

func (d Deps) Clone(s page.Site, conf config.AllProvider) (*Deps, error) {
	d.Conf = conf
	d.Site = s
	d.ExecHelper = nil
	d.ContentSpec = nil

	if err := d.Init(); err != nil {
		return nil, err
	}

	return &d, nil
}

func (d *Deps) GetTemplateStore() *tplimpl.TemplateStore {
	return d.TemplateStore
}

func (d *Deps) Init() error {
	if d.Conf == nil {
		panic("conf is nil")
	}

	if d.Fs == nil {
		// For tests.
		d.Fs = hugofs.NewFrom(afero.NewMemMapFs(), d.Conf.BaseConfig())
	}

	if d.Log == nil {
		d.Log = loggers.NewDefault()
	}

	if d.globalErrHandler == nil {
		d.globalErrHandler = &globalErrHandler{
			logger: d.Log,
		}
	}
	if d.BuildState == nil {
		d.BuildState = &BuildState{}
	}
	if d.Counters == nil {
		d.Counters = &Counters{}
	}
	if d.BuildState.DeferredExecutions == nil {
		if d.BuildState.DeferredExecutionsGroupedByRenderingContext == nil {
			d.BuildState.DeferredExecutionsGroupedByRenderingContext = make(map[tpl.RenderingContext]*DeferredExecutions)
		}
		d.BuildState.DeferredExecutions = &DeferredExecutions{
			Executions:              hmaps.NewCache[string, *tpl.DeferredExecution](),
			FilenamesWithPostPrefix: hmaps.NewCache[string, bool](),
		}
	}

	if d.BuildStartListeners == nil {
		d.BuildStartListeners = &Listeners[any]{}
	}

	if d.BuildEndListeners == nil {
		d.BuildEndListeners = &Listeners[any]{}
	}

	if d.BuildClosers == nil {
		d.BuildClosers = &types.Closers{}
	}

	if d.OnChangeListeners == nil {
		d.OnChangeListeners = &Listeners[identity.Identity]{}
	}

	if d.Metrics == nil && d.Conf.TemplateMetrics() {
		d.Metrics = metrics.NewProvider(d.Conf.TemplateMetricsHints())
	}

	if d.ExecHelper == nil {
		d.ExecHelper = hexec.New(d.Conf.GetConfigSection("security").(security.Config), d.Conf.WorkingDir(), d.Log)
	}

	if d.MemCache == nil {
		d.MemCache = dynacache.New(dynacache.Options{Watching: d.Conf.Watching(), Log: d.Log})
	}

	if d.PathSpec == nil {
		hashBytesReceiverFunc := func(name string, match []byte) {
			s := string(match)
			switch s {
			case postpub.PostProcessPrefix:
				d.BuildState.AddFilenameWithPostPrefix(name)
			case tpl.HugoDeferredTemplatePrefix:
				d.BuildState.DeferredExecutions.FilenamesWithPostPrefix.Set(name, true)
			}
		}

		// Skip binary files.
		mediaTypes := d.Conf.GetConfigSection("mediaTypes").(media.Types)
		hashBytesShouldCheck := func(name string) bool {
			ext := strings.TrimPrefix(filepath.Ext(name), ".")
			return mediaTypes.IsTextSuffix(ext)
		}
		d.Fs.PublishDir = hugofs.NewHasBytesReceiver(
			d.Fs.PublishDir,
			hashBytesShouldCheck,
			hashBytesReceiverFunc,
			[]byte(tpl.HugoDeferredTemplatePrefix),
			[]byte(postpub.PostProcessPrefix))

		pathSpec, err := helpers.NewPathSpec(d.Fs, d.Conf, d.Log, nil)
		if err != nil {
			return err
		}
		d.PathSpec = pathSpec
	} else {
		var err error
		d.PathSpec, err = helpers.NewPathSpec(d.Fs, d.Conf, d.Log, d.PathSpec.BaseFs)
		if err != nil {
			return err
		}
	}

	d.ExecHelper.SetNodeReadPaths(d.BaseFs.Assets.RealPaths(""))

	if d.ContentSpec == nil {
		contentSpec, err := helpers.NewContentSpec(d.Conf, d.Log, d.Content.Fs, d.ExecHelper)
		if err != nil {
			return err
		}
		d.ContentSpec = contentSpec
	}

	if d.SourceSpec == nil {
		d.SourceSpec = source.NewSourceSpec(d.PathSpec, nil, d.Fs.Source)
	}

	var common *resources.SpecCommon
	if d.ResourceSpec != nil {
		common = d.ResourceSpec.SpecCommon
	}

	d.Cfg.BaseConfig()

	fileCaches := d.Cfg.FileCaches().(filecache.Caches)
	fileCaches.SetResourceFs(d.BaseFs.ResourcesCache)

	resourceSpec, err := resources.NewSpec(d.PathSpec, common, d.WasmDispatchers, fileCaches, d.MemCache, d.BuildState, d.Log, d, d.ExecHelper, d.BuildClosers, d.BuildState)
	if err != nil {
		return fmt.Errorf("failed to create resource spec: %w", err)
	}
	d.ResourceSpec = resourceSpec

	return nil
}

// TODO(bep) rework this to get it in line with how we manage templates.
func (d *Deps) Compile(prototype *Deps) error {
	var err error
	if prototype == nil {

		if err = d.TranslationProvider.NewResource(d); err != nil {
			return err
		}
		return nil
	}

	if err = d.TranslationProvider.CloneResource(d, prototype); err != nil {
		return err
	}

	return nil
}

// MkdirTemp returns a temporary directory path that will be cleaned up on exit.
func (d Deps) MkdirTemp(pattern string) (string, error) {
	filename, err := os.MkdirTemp("", pattern)
	if err != nil {
		return "", err
	}
	d.BuildClosers.Add(
		types.CloserFunc(
			func() error {
				return os.RemoveAll(filename)
			},
		),
	)

	return filename, nil
}

type globalErrHandler struct {
	logger loggers.Logger

	// Channel for some "hard to get to" build errors
	buildErrors chan error
	// Used to signal that the build is done.
	quit chan struct{}
}

// SendError sends the error on a channel to be handled later.
// This can be used in situations where returning and aborting the current
// operation isn't practical.
func (e *globalErrHandler) SendError(err error) {
	if e.buildErrors != nil {
		select {
		case <-e.quit:
		case e.buildErrors <- err:
		default:
		}
		return
	}
	e.logger.Errorln(err)
}

func (e *globalErrHandler) StartErrorCollector() chan error {
	e.quit = make(chan struct{})
	e.buildErrors = make(chan error, 10)
	return e.buildErrors
}

func (e *globalErrHandler) StopErrorCollector() {
	if e.buildErrors != nil {
		close(e.quit)
		close(e.buildErrors)
	}
}

// Listeners represents an event listener.
type Listeners[T any] struct {
	sync.Mutex

	// A list of funcs to be notified about an event.
	// If the return value is true, the listener will be removed.
	listeners []func(...T) bool
}

// Add adds a function to a Listeners instance.
func (b *Listeners[T]) Add(f func(...T) bool) {
	if b == nil {
		return
	}
	b.Lock()
	defer b.Unlock()
	b.listeners = append(b.listeners, f)
}

// Notify executes all listener functions.
func (b *Listeners[T]) Notify(vs ...T) {
	b.Lock()
	defer b.Unlock()
	temp := b.listeners[:0]
	for _, notify := range b.listeners {
		if !notify(vs...) {
			temp = append(temp, notify)
		}
	}
	b.listeners = temp
}

// ResourceProvider is used to create and refresh, and clone resources needed.
type ResourceProvider interface {
	NewResource(dst *Deps) error
	CloneResource(dst, src *Deps) error
}

func (d *Deps) Close() error {
	if d.isClosed {
		return nil
	}
	d.isClosed = true

	if d.MemCache != nil {
		d.MemCache.Stop()
	}
	if d.WasmDispatchers != nil {
		d.WasmDispatchers.Close()
	}
	return d.BuildClosers.Close()
}

// DepsCfg contains configuration options that can be used to configure Hugo
// on a global level, i.e. logging etc.
// Nil values will be given default values.
type DepsCfg struct {
	// The logger to use. Only set in some tests.
	// TODO(bep) get rid of this.
	TestLogger loggers.Logger

	// The logging level to use.
	LogLevel logg.Level

	// Logging output.
	StdErr io.Writer

	// The console output.
	StdOut io.Writer

	// The file systems to use
	Fs *hugofs.Fs

	// The Site in use
	Site page.Site

	Configs *allconfig.Configs

	// Template handling.
	TemplateProvider ResourceProvider

	// i18n handling.
	TranslationProvider ResourceProvider

	// Build triggered by the IntegrationTest framework.
	IsIntegrationTest bool

	// TestCfg holds configuration used only in tests.
	// It is a programming error to set this when IsIntegrationTest is not set,
	// and doing so will panic.
	TestCfg TestConfig

	// ChangesFromBuild for changes passed back to the server/watch process.
	ChangesFromBuild chan []identity.Identity
}

// TestConfig holds configuration used only in tests.
// See DepsCfg.TestCfg.
type TestConfig struct {
	// WarpcMemory, if set, overrides the memory limit in MiB for the WASM based
	// image processors (WebP and AVIF). Used to provoke memory allocation failures.
	WarpcMemory int
}

// IsZero reports whether c holds no test configuration.
func (c TestConfig) IsZero() bool {
	return c == TestConfig{}
}

// BuildState are state used during a build.
type BuildState struct {
	counter uint64

	// Tracks invocations of the Build method.
	BuildCounter atomic.Uint64

	mu sync.Mutex // protects state below.

	OnSignalRebuild func(ids ...identity.Identity)

	// A set of filenames in /public that
	// contains a post-processing prefix.
	filenamesWithPostPrefix map[string]bool

	DeferredExecutions *DeferredExecutions

	// Deferred executions grouped by rendering context.
	DeferredExecutionsGroupedByRenderingContext map[tpl.RenderingContext]*DeferredExecutions
}

// Misc counters.
type Counters struct {
	// Counter for the math.Counter function.
	MathCounter atomic.Uint64
}

type DeferredExecutions struct {
	// A set of filenames in /public that
	// contains a post-processing prefix.
	FilenamesWithPostPrefix *hmaps.Cache[string, bool]

	// Maps a placeholder to a deferred execution.
	Executions *hmaps.Cache[string, *tpl.DeferredExecution]
}

var _ identity.SignalRebuilder = (*BuildState)(nil)

// IsRebuild reports whether this is a rebuild.
func (b *BuildState) IsRebuild() bool {
	return b.BuildCounter.Load() > 0
}

// StartStageRender will be called before a stage is rendered.
func (b *BuildState) StartStageRender(stage tpl.RenderingContext) {
}

// StopStageRender will be called after a stage is rendered.
func (b *BuildState) StopStageRender(stage tpl.RenderingContext) {
	b.DeferredExecutionsGroupedByRenderingContext[stage] = b.DeferredExecutions
	b.DeferredExecutions = &DeferredExecutions{
		Executions:              hmaps.NewCache[string, *tpl.DeferredExecution](),
		FilenamesWithPostPrefix: hmaps.NewCache[string, bool](),
	}
}

func (b *BuildState) SignalRebuild(ids ...identity.Identity) {
	b.OnSignalRebuild(ids...)
}

func (b *BuildState) AddFilenameWithPostPrefix(filename string) {
	b.mu.Lock()
	defer b.mu.Unlock()
	if b.filenamesWithPostPrefix == nil {
		b.filenamesWithPostPrefix = make(map[string]bool)
	}
	b.filenamesWithPostPrefix[filename] = true
}

func (b *BuildState) GetFilenamesWithPostPrefix() []string {
	b.mu.Lock()
	defer b.mu.Unlock()
	var filenames []string
	for filename := range b.filenamesWithPostPrefix {
		filenames = append(filenames, filename)
	}
	sort.Strings(filenames)
	return filenames
}

func (b *BuildState) Incr() int {
	return int(atomic.AddUint64(&b.counter, uint64(1)))
}
