// 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 hugolib

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path"
	"path/filepath"
	"strings"
	"time"

	"github.com/bep/debounce"
	"github.com/bep/helpers/maphelpers"
	"github.com/bep/logg"
	"github.com/gohugoio/go-radix"
	"github.com/gohugoio/hugo/bufferpool"
	"github.com/gohugoio/hugo/deps"
	"github.com/gohugoio/hugo/hugofs"
	"github.com/gohugoio/hugo/hugofs/files"
	"github.com/gohugoio/hugo/hugofs/hglob"
	"github.com/gohugoio/hugo/hugolib/doctree"
	"github.com/gohugoio/hugo/hugolib/pagesfromdata"
	"github.com/gohugoio/hugo/hugolib/segments"
	"github.com/gohugoio/hugo/identity"
	"github.com/gohugoio/hugo/output"
	"github.com/gohugoio/hugo/publisher"
	"github.com/gohugoio/hugo/source"
	"github.com/gohugoio/hugo/tpl"

	"github.com/gohugoio/hugo/common/herrors"
	"github.com/gohugoio/hugo/common/loggers"
	"github.com/gohugoio/hugo/common/para"
	"github.com/gohugoio/hugo/common/paths"
	"github.com/gohugoio/hugo/common/rungroup"
	"github.com/gohugoio/hugo/common/terminal"
	"github.com/gohugoio/hugo/config"
	"github.com/gohugoio/hugo/resources/page"
	"github.com/gohugoio/hugo/resources/page/siteidentities"
	"github.com/gohugoio/hugo/resources/postpub"

	"github.com/spf13/afero"

	"github.com/fsnotify/fsnotify"
)

// Build builds all sites. If filesystem events are provided,
// this is considered to be a potential partial rebuild.
func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
	if h.isRebuild() && !h.Conf.Watching() && !h.Conf.Running() {
		return errors.New("Build called multiple times when not in watch or server mode (typically with hugolib.Test(t, files).Build(); Build() is already called once by Test)")
	}
	if !h.isRebuild() && terminal.PrintANSIColors(os.Stdout) {
		// Don't show progress for fast builds.
		d := debounce.New(250 * time.Millisecond)
		d(func() {
			h.progressReporter.Start()
			h.reportProgress(func() (state terminal.ProgressState, progress float64) {
				// We don't know how many files to process below, so use the intermediate state as the first progress.
				return terminal.ProgressIntermediate, 1.0
			})
		})
		defer d(func() {})
	}

	infol := h.Log.InfoCommand("build")
	defer loggers.TimeTrackf(infol, time.Now(), nil, "")
	defer func() {
		h.reportProgress(func() (state terminal.ProgressState, progress float64) {
			return terminal.ProgressHidden, 1.0
		})
		h.BuildState.BuildCounter.Add(1)
	}()

	if h.Deps == nil {
		panic("must have deps")
	}

	if !config.NoBuildLock {
		unlock, err := h.BaseFs.LockBuild()
		if err != nil {
			return fmt.Errorf("failed to acquire a build lock: %w", err)
		}
		defer unlock()
	}

	defer func() {
		for _, s := range h.Sites {
			s.Deps.BuildEndListeners.Notify()
		}
	}()

	errCollector := h.StartErrorCollector()
	errs := make(chan error)

	go func(from, to chan error) {
		var errors []error
		i := 0
		for e := range from {
			i++
			if i > 50 {
				break
			}
			errors = append(errors, e)
		}
		to <- h.filterAndJoinErrors(errors)

		close(to)
	}(errCollector, errs)

	for _, s := range h.Sites {
		s.state = siteStateInit
	}

	if h.Metrics != nil {
		h.Metrics.Reset()
	}

	h.buildCounters = config.testCounters
	if h.buildCounters == nil {
		h.buildCounters = &buildCounters{}
	}

	// Need a pointer as this may be modified.
	conf := &config
	if conf.WhatChanged == nil {
		// Assume everything has changed
		conf.WhatChanged = &WhatChanged{needsPagesAssembly: true}
	}

	var prepareErr error

	if !config.PartialReRender {
		prepare := func() error {
			init := func(conf *BuildCfg) error {
				for _, s := range h.Sites {
					s.Deps.BuildStartListeners.Notify()
				}

				if len(events) > 0 || len(conf.WhatChanged.Changes()) > 0 {
					// Rebuild
					if err := h.initRebuild(conf); err != nil {
						return fmt.Errorf("initRebuild: %w", err)
					}
				} else {
					if err := h.initSites(); err != nil {
						return fmt.Errorf("initSites: %w", err)
					}
				}

				return nil
			}

			ctx := context.Background()

			if err := h.process(ctx, infol, conf, init, events...); err != nil {
				return fmt.Errorf("process: %w", err)
			}
			h.reportProgress(func() (state terminal.ProgressState, progress float64) {
				return terminal.ProgressNormal, 0.15
			})
			if err := h.assemble(ctx, infol, conf); err != nil {
				return fmt.Errorf("assemble: %w", err)
			}
			h.reportProgress(func() (state terminal.ProgressState, progress float64) {
				return terminal.ProgressNormal, 0.20
			})

			return nil
		}

		if prepareErr = prepare(); prepareErr != nil {
			h.SendError(prepareErr)
		}
	}

	for s := range h.allSites(nil) {
		s.state = siteStateReady
	}

	if prepareErr == nil {
		if err := h.render(infol, conf); err != nil {
			h.SendError(fmt.Errorf("render: %w", err))
		}

		// Make sure to write any build stats to disk first so it's available
		// to the post processors.
		if err := h.writeBuildStats(); err != nil {
			return err
		}

		// We need to do this before render deferred.
		if err := h.printPathWarningsOnce(); err != nil {
			h.SendError(fmt.Errorf("printPathWarnings: %w", err))
		}

		if err := h.renderDeferred(infol); err != nil {
			h.SendError(fmt.Errorf("renderDeferred: %w", err))
		}

		// This needs to be done after the deferred rendering to get complete template usage coverage.
		if err := h.printUnusedTemplatesOnce(); err != nil {
			h.SendError(fmt.Errorf("printPathWarnings: %w", err))
		}

		if err := h.postProcess(infol); err != nil {
			h.SendError(fmt.Errorf("postProcess: %w", err))
		}
	}

	if h.Metrics != nil {
		var b bytes.Buffer
		h.Metrics.WriteMetrics(&b)

		h.Log.Printf("\nTemplate Metrics:\n\n")
		h.Log.Println(b.String())
	}

	h.StopErrorCollector()

	err := <-errs
	if err != nil {
		return err
	}

	if err := h.fatalErrorHandler.getErr(); err != nil {
		return err
	}

	errorCount := h.Log.LoggCount(logg.LevelError) + loggers.Log().LoggCount(logg.LevelError)
	if errorCount > 0 {
		return fmt.Errorf("logged %d error(s)", errorCount)
	}

	return nil
}

// Build lifecycle methods below.
// The order listed matches the order of execution.

func (h *HugoSites) initSites() error {
	h.reset()
	return nil
}

func (h *HugoSites) initRebuild(config *BuildCfg) error {
	if !h.Configs.Base.Internal.Watch {
		return errors.New("rebuild called when not in watch mode")
	}

	h.pageTrees.treePagesResources.WalkPrefixRaw("", func(key string, n contentNode) (radix.WalkFlag, contentNode, error) {
		cnh.resetBuildState(n)
		return radix.WalkContinue, nil, nil
	})

	for _, s := range h.Sites {
		s.resetBuildState(config.WhatChanged.needsPagesAssembly)
	}

	h.reset()
	h.resetLogs()

	return nil
}

// process prepares the Sites' sources for a full or partial rebuild.
// This will also parse the source and create all the Page objects.
func (h *HugoSites) process(ctx context.Context, l logg.LevelLogger, config *BuildCfg, init func(config *BuildCfg) error, events ...fsnotify.Event) error {
	l = l.WithField("step", "process")
	defer loggers.TimeTrackf(l, time.Now(), nil, "")

	if len(events) > 0 {
		// This is a rebuild triggered from file events.
		return h.processPartialFileEvents(ctx, l, config, init, events)
	} else if len(config.WhatChanged.Changes()) > 0 {
		// Rebuild triggered from remote events.
		if err := init(config); err != nil {
			return err
		}
		return h.processPartialRebuildChanges(ctx, l, config)
	}
	return h.processFull(ctx, l, config)
}

// assemble creates missing sections, applies aggregate values (e.g. dates, cascading params),
// removes disabled pages etc.
func (h *HugoSites) assemble(ctx context.Context, l logg.LevelLogger, bcfg *BuildCfg) error {
	l = l.WithField("step", "assemble")
	defer loggers.TimeTrackf(l, time.Now(), nil, "")

	if !bcfg.WhatChanged.needsPagesAssembly {
		changes := bcfg.WhatChanged.Drain()
		if len(changes) > 0 {
			if err := h.resolveAndClearStateForIdentities(ctx, l, nil, changes); err != nil {
				return err
			}
		}
		return nil
	}

	h.translationKeyPages.Reset()

	var assemblers []*sitePagesAssembler
	// Changes detected during assembly (e.g. aggregate date changes)
	for s := range h.allSites(nil) {
		assemblers = append(assemblers, &sitePagesAssembler{
			s:               s,
			assembleChanges: bcfg.WhatChanged,
			ctx:             ctx,
		})
	}

	apa := newAllPagesAssembler(
		ctx,
		h,
		assemblers[0].s.pageMap,
		bcfg.WhatChanged,
	)
	for _, s := range assemblers {
		s.a = apa
	}

	if h.Conf.Watching() {
		defer func() {
			// Store previous walk context to detect cascade changes on next rebuild.
			h.previousPageTreesWalkContext = apa.rwRoot.WalkContext
		}()
	}

	if err := apa.createAllPages(); err != nil {
		return err
	}

	g, _ := h.workersSite.Start(ctx)
	for _, s := range assemblers {
		g.Run(func() error {
			return s.assemblePagesStep1()
		})
	}
	if err := g.Wait(); err != nil {
		return err
	}

	changes := bcfg.WhatChanged.Drain()

	// Changes from the assemble step (e.g. lastMod, cascade) needs a re-calculation
	// of what needs to be re-built.
	if len(changes) > 0 {
		if err := h.resolveAndClearStateForIdentities(ctx, l, nil, changes); err != nil {
			return err
		}
	}

	for _, s := range assemblers {
		if err := s.assemblePagesStep2(); err != nil {
			return err
		}
	}

	// Handle new terms from assemblePagesStep2.
	changes = bcfg.WhatChanged.Drain()
	if len(changes) > 0 {
		if err := h.resolveAndClearStateForIdentities(ctx, l, nil, changes); err != nil {
			return err
		}
	}

	h.renderFormats = output.Formats{}
	for s := range h.allSites(nil) {
		s.initRenderFormats()
		h.renderFormats = append(h.renderFormats, s.renderFormats...)
	}

	for _, s := range assemblers {
		if err := s.assemblePagesStepFinal(); err != nil {
			return err
		}
	}

	return nil
}

// render renders the sites.
func (h *HugoSites) render(l logg.LevelLogger, config *BuildCfg) error {
	l = l.WithField("step", "render")
	start := time.Now()
	defer func() {
		loggers.TimeTrackf(l, start, h.buildCounters.loggFields(), "")
	}()

	siteRenderContext := &siteRenderContext{cfg: config, infol: l, multihost: h.Configs.IsMultihost}

	renderErr := func(err error) error {
		if err == nil {
			return nil
		}
		// In Hugo 0.141.0 we replaced the special error handling for resources.GetRemote
		// with the more general try.
		if strings.Contains(err.Error(), "can't evaluate field Err in type") {
			if strings.Contains(err.Error(), "resource.Resource") {
				return fmt.Errorf("%s: Resource.Err was removed in Hugo v0.141.0 and replaced with a new try keyword, see https://gohugo.io/functions/go-template/try/", err)
			} else if strings.Contains(err.Error(), "template.HTML") {
				return fmt.Errorf("%s: the return type of transform.ToMath was changed in Hugo v0.141.0 and the error handling replaced with a new try keyword, see https://gohugo.io/functions/go-template/try/", err)
			}
		}
		return err
	}

	i := 0

	for s := range h.allSites(nil) {
		if s.conf.Segments.Config.SegmentFilter.ShouldExcludeCoarse(segments.SegmentQuery{Site: s.siteVector}) {
			l.Logf("skip site %s not matching segments set in --renderSegments", s.resolveDimensionNames())
			continue
		}
		siteRenderContext.languageIdx = s.siteVector.Language()
		h.currentSite = s
		for siteOutIdx, renderFormat := range s.renderFormats {
			if s.conf.Segments.Config.SegmentFilter.ShouldExcludeCoarse(segments.SegmentQuery{Output: renderFormat.Name, Site: s.siteVector}) {
				l.Logf("skip output format %q for site %s not matching segments set in --renderSegments", renderFormat.Name, s.resolveDimensionNames())
				continue
			}

			if err := func() error {
				rc := tpl.RenderingContext{Site: s, SiteOutIdx: siteOutIdx}
				h.BuildState.StartStageRender(rc)
				defer h.BuildState.StopStageRender(rc)

				siteRenderContext.outIdx = siteOutIdx
				siteRenderContext.sitesOutIdx = i
				i++

				select {
				case <-h.Done():
					return nil
				default:
					for s2 := range h.allSites(nil) {
						if err := s2.preparePagesForRender(s == s2, siteRenderContext.sitesOutIdx); err != nil {
							return err
						}
					}
					if !config.SkipRender {
						ll := l.WithField("substep", "pages").
							WithField("site", s.language.Lang).
							WithField("outputFormat", renderFormat.Name)

						start := time.Now()

						if config.PartialReRender {
							if err := s.renderPages(siteRenderContext); err != nil {
								return err
							}
						} else {
							if err := s.render(siteRenderContext); err != nil {
								return renderErr(err)
							}
						}
						loggers.TimeTrackf(ll, start, nil, "")
					}
				}
				return nil
			}(); err != nil {
				return err
			}

		}

	}

	return nil
}

func (h *HugoSites) renderDeferred(l logg.LevelLogger) error {
	l = l.WithField("step", "render deferred")
	start := time.Now()

	var deferredCount int

	for rc, de := range h.Deps.BuildState.DeferredExecutionsGroupedByRenderingContext {
		if de.FilenamesWithPostPrefix.Len() == 0 {
			continue
		}

		deferredCount += de.FilenamesWithPostPrefix.Len()

		s := rc.Site.(*Site)
		for _, s2 := range h.Sites {
			if err := s2.preparePagesForRender(s == s2, rc.SiteOutIdx); err != nil {
				return err
			}
		}
		if err := s.executeDeferredTemplates(de); err != nil {
			return herrors.ImproveRenderErr(err)
		}
	}

	loggers.TimeTrackf(l, start, logg.Fields{
		logg.Field{Name: "count", Value: deferredCount},
	}, "")

	return nil
}

func (s *Site) executeDeferredTemplates(de *deps.DeferredExecutions) error {
	handleFile := func(filename string) error {
		content, err := afero.ReadFile(s.BaseFs.PublishFs, filename)
		if err != nil {
			return err
		}

		k := 0
		changed := false

		for {
			if k >= len(content) {
				break
			}
			l := bytes.Index(content[k:], []byte(tpl.HugoDeferredTemplatePrefix))
			if l == -1 {
				break
			}
			m := bytes.Index(content[k+l:], []byte(tpl.HugoDeferredTemplateSuffix)) + len(tpl.HugoDeferredTemplateSuffix)

			low, high := k+l, k+l+m

			forward := l + m
			id := string(content[low:high])

			if err := func() error {
				deferred, found := de.Executions.Get(id)
				if !found {
					panic(fmt.Sprintf("deferred execution with id %q not found", id))
				}
				deferred.Mu.Lock()
				defer deferred.Mu.Unlock()

				if !deferred.Executed {
					tmpl := s.Deps.GetTemplateStore()
					ti := s.TemplateStore.LookupByPath(deferred.TemplatePath)
					if ti == nil {
						panic(fmt.Sprintf("template %q not found", deferred.TemplatePath))
					}

					if err := func() error {
						buf := bufferpool.GetBuffer()
						defer bufferpool.PutBuffer(buf)

						err = tmpl.ExecuteWithContext(deferred.Ctx, ti, buf, deferred.Data)
						if err != nil {
							return err
						}
						deferred.Result = buf.String()
						deferred.Executed = true

						return nil
					}(); err != nil {
						return err
					}
				}

				content = append(content[:low], append([]byte(deferred.Result), content[high:]...)...)
				forward = len(deferred.Result)
				changed = true

				return nil
			}(); err != nil {
				return err
			}

			k += forward
		}

		if changed {
			return afero.WriteFile(s.BaseFs.PublishFs, filename, content, 0o666)
		}

		return nil
	}

	g := rungroup.Run(context.Background(), rungroup.Config[string]{
		NumWorkers: s.h.numWorkers,
		Handle: func(ctx context.Context, filename string) error {
			return handleFile(filename)
		},
	})

	de.FilenamesWithPostPrefix.ForEeach(func(filename string, _ bool) bool {
		g.Enqueue(filename)
		return true
	})

	return g.Wait()
}

// printPathWarningsOnce prints path warnings if enabled.
func (h *HugoSites) printPathWarningsOnce() error {
	h.printPathWarningsInit.Do(func() {
		conf := h.Configs.Base
		if conf.PrintPathWarnings {
			// We need to do this before any post processing, as that may write to the same files twice
			// and create false positives.
			hugofs.WalkFilesystems(h.Fs.PublishDir, func(fs afero.Fs) bool {
				if dfs, ok := fs.(hugofs.DuplicatesReporter); ok {
					dupes := dfs.ReportDuplicates()
					if dupes != "" {
						h.Log.Warnln("Duplicate target paths:", dupes)
					}
				}
				return false
			})
		}
	})
	return nil
}

// / printUnusedTemplatesOnce prints unused templates if enabled.
func (h *HugoSites) printUnusedTemplatesOnce() error {
	h.printUnusedTemplatesInit.Do(func() {
		conf := h.Configs.Base
		if conf.PrintUnusedTemplates {
			unusedTemplates := h.GetTemplateStore().UnusedTemplates()
			for _, unusedTemplate := range unusedTemplates {
				if unusedTemplate.Fi != nil {
					h.Log.Warnf("Template %s is unused, source %q", unusedTemplate.PathInfo.Path(), unusedTemplate.Fi.Meta().Filename)
				} else {
					h.Log.Warnf("Template %s is unused", unusedTemplate.PathInfo.Path())
				}
			}
		}
	})
	return nil
}

// postProcess runs the post processors, e.g. writing the hugo_stats.json file.
func (h *HugoSites) postProcess(l logg.LevelLogger) error {
	l = l.WithField("step", "postProcess")
	defer loggers.TimeTrackf(l, time.Now(), nil, "")

	// This will only be set when js.Build have been triggered with
	// imports that resolves to the project or a module.
	// Write a jsconfig.json file to the project's /asset directory
	// to help JS IntelliSense in VS Code etc.
	if !h.ResourceSpec.BuildConfig().NoJSConfigInAssets {
		handleJSConfig := func(fi os.FileInfo) {
			m := fi.(hugofs.FileMetaInfo).Meta()
			if !m.IsProject {
				return
			}

			if jsConfig := h.ResourceSpec.JSConfigBuilder.Build(m.SourceRoot); jsConfig != nil {
				b, err := json.MarshalIndent(jsConfig, "", " ")
				if err != nil {
					h.Log.Warnf("Failed to create jsconfig.json: %s", err)
				} else {
					filename := filepath.Join(m.SourceRoot, "jsconfig.json")
					if h.Configs.Base.Internal.Running {
						h.skipRebuildForFilenamesMu.Lock()
						h.skipRebuildForFilenames[filename] = true
						h.skipRebuildForFilenamesMu.Unlock()
					}
					// Make sure it's  written to the OS fs as this is used by
					// editors.
					if err := afero.WriteFile(hugofs.Os, filename, b, 0o666); err != nil {
						h.Log.Warnf("Failed to write jsconfig.json: %s", err)
					}
				}
			}
		}

		fi, err := h.BaseFs.Assets.Fs.Stat("")
		if err != nil {
			if !herrors.IsNotExist(err) {
				h.Log.Warnf("Failed to resolve jsconfig.json dir: %s", err)
			}
		} else {
			handleJSConfig(fi)
		}
	}

	var toPostProcess []postpub.PostPublishedResource
	for _, r := range h.ResourceSpec.PostProcessResources.All() {
		toPostProcess = append(toPostProcess, r)
	}

	if len(toPostProcess) == 0 {
		// Nothing more to do.
		return nil
	}

	workers := para.New(config.GetNumWorkerMultiplier())
	g, _ := workers.Start(context.Background())

	handleFile := func(filename string) error {
		content, err := afero.ReadFile(h.BaseFs.PublishFs, filename)
		if err != nil {
			return err
		}

		k := 0
		changed := false

		for {
			l := bytes.Index(content[k:], []byte(postpub.PostProcessPrefix))
			if l == -1 {
				break
			}
			m := bytes.Index(content[k+l:], []byte(postpub.PostProcessSuffix)) + len(postpub.PostProcessSuffix)

			low, high := k+l, k+l+m

			field := content[low:high]

			forward := l + m

			for i, r := range toPostProcess {
				if r == nil {
					panic(fmt.Sprintf("resource %d to post process is nil", i+1))
				}
				v, ok := r.GetFieldString(string(field))
				if ok {
					content = append(content[:low], append([]byte(v), content[high:]...)...)
					changed = true
					forward = len(v)
					break
				}
			}

			k += forward
		}

		if changed {
			return afero.WriteFile(h.BaseFs.PublishFs, filename, content, 0o666)
		}

		return nil
	}

	filenames := h.Deps.BuildState.GetFilenamesWithPostPrefix()
	for _, filename := range filenames {
		g.Run(func() error {
			return handleFile(filename)
		})
	}

	// Prepare for a new build.
	for _, s := range h.Sites {
		s.ResourceSpec.PostProcessResources = maphelpers.NewConcurrentMap[string, postpub.PostPublishedResource]()
	}

	return g.Wait()
}

func (h *HugoSites) writeBuildStats() error {
	if h.ResourceSpec == nil {
		panic("h.ResourceSpec is nil")
	}
	if !h.ResourceSpec.BuildConfig().BuildStats.Enabled() {
		return nil
	}

	htmlElements := &publisher.HTMLElements{}
	for _, s := range h.Sites {
		stats := s.publisher.PublishStats()
		htmlElements.Merge(stats.HTMLElements)
	}

	filename := filepath.Join(h.Configs.LoadingInfo.BaseConfig.WorkingDir, files.FilenameHugoStatsJSON)

	existingContent, _ := afero.ReadFile(hugofs.Os, filename)

	// When rendering only a subset of the site, merge with any existing
	// hugo_stats.json so that elements from pages not rendered in this build
	// are preserved (e.g. so Tailwind doesn't strip their classes).
	// See issue 14939.
	if len(h.Configs.Base.RenderSegments) > 0 && len(existingContent) > 0 {
		var existing publisher.PublishStats
		if err := json.Unmarshal(existingContent, &existing); err == nil {
			htmlElements.Merge(existing.HTMLElements)
		} else {
			h.Log.Warnf("Failed to unmarshal existing hugo_stats.json: %s", err)
		}
	}

	htmlElements.Sort()

	stats := publisher.PublishStats{
		HTMLElements: *htmlElements,
	}

	var buf bytes.Buffer
	enc := json.NewEncoder(&buf)
	enc.SetEscapeHTML(false)
	enc.SetIndent("", "  ")
	err := enc.Encode(stats)
	if err != nil {
		return err
	}
	js := buf.Bytes()

	if bytes.Equal(existingContent, js) {
		return nil
	}

	// Make sure it's always written to the OS fs.
	if err := afero.WriteFile(hugofs.Os, filename, js, 0o666); err != nil {
		return err
	}

	// Write to the destination as well if it's a in-memory fs.
	if !hugofs.IsOsFs(h.Fs.Source) {
		if err := afero.WriteFile(h.Fs.WorkingDirWritable, filename, js, 0o666); err != nil {
			return err
		}
	}

	// This step may be followed by a post process step that may
	// rebuild e.g. CSS, so clear any cache that's defined for the hugo_stats.json.
	h.dynacacheGCFilenameIfNotWatchedAndDrainMatching(filename)

	return nil
}

type pathChange struct {
	// The path to the changed file.
	p *paths.Path

	// If true, this is a structural change (e.g. a delete or a rename).
	structural bool

	// If true, this is a directory.
	isDir bool
}

func (p pathChange) isStructuralChange() bool {
	return p.structural || p.isDir
}

func (h *HugoSites) processPartialRebuildChanges(ctx context.Context, l logg.LevelLogger, config *BuildCfg) error {
	if err := h.resolveAndClearStateForIdentities(ctx, l, nil, config.WhatChanged.Drain()); err != nil {
		return err
	}

	if err := h.processContentAdaptersOnRebuild(ctx, config); err != nil {
		return err
	}
	return nil
}

// processPartialFileEvents prepares the Sites' sources for a partial rebuild.
func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLogger, config *BuildCfg, init func(config *BuildCfg) error, events []fsnotify.Event) error {
	h.Log.Trace(logg.StringFunc(func() string {
		var sb strings.Builder
		sb.WriteString("File events:\n")
		for _, ev := range events {
			sb.WriteString(ev.String())
			sb.WriteString("\n")
		}
		return sb.String()
	}))

	// For a list of events for the different OSes, see the test output in https://github.com/bep/fsnotifyeventlister/.
	events = h.fileEventsFilter(events)
	events = h.fileEventsTrim(events)
	eventInfos := h.fileEventsApplyInfo(events)

	logger := h.Log

	var (
		tmplAdded          bool
		tmplChanged        bool
		i18nChanged        bool
		needsPagesAssemble bool
	)

	changedPaths := struct {
		changedFiles []*paths.Path
		addedFiles   []*paths.Path
		changedDirs  []*paths.Path
		deleted      []*paths.Path
	}{}

	removeDuplicatePaths := func(ps []*paths.Path) []*paths.Path {
		seen := make(map[string]bool)
		var filtered []*paths.Path
		for _, p := range ps {
			if !seen[p.Path()] {
				seen[p.Path()] = true
				filtered = append(filtered, p)
			}
		}
		return filtered
	}

	var (
		cacheBusters      []func(string) bool
		deletedDirs       []string
		addedContentPaths []*paths.Path
	)

	var (
		addedOrChangedContent []pathChange
		changes               []identity.Identity
	)

	for _, ev := range eventInfos {
		cpss := h.BaseFs.ResolvePaths(ev.Name)
		pss := make([]*paths.Path, len(cpss))
		for i, cps := range cpss {
			p := cps.Path
			if ev.removed && !paths.HasExt(p) {
				// Assume this is a renamed/removed directory.
				// For deletes, we walk up the tree to find the container (e.g. branch bundle),
				// so we will catch this even if it is a file without extension.
				// This avoids us walking up to the home page bundle for the common case
				// of renaming root sections.
				p = p + "/_index.md"
				deletedDirs = append(deletedDirs, cps.Path)
			}

			pss[i] = h.Configs.ContentPathParser.Parse(cps.Component, p)
			if ev.added && !ev.isChangedDir && cps.Component == files.ComponentFolderContent {
				addedContentPaths = append(addedContentPaths, pss[i])
			}

			// Compile cache buster.
			np := hglob.NormalizePath(path.Join(cps.Component, cps.Path))
			g, err := h.ResourceSpec.BuildConfig().MatchCacheBuster(h.Log, np)
			if err == nil && g != nil {
				cacheBusters = append(cacheBusters, g)
			}

			if ev.added {
				changes = append(changes, identity.StructuralChangeAdd)
			}
			if ev.removed {
				changes = append(changes, identity.StructuralChangeRemove)
			}
		}

		if ev.removed {
			changedPaths.deleted = append(changedPaths.deleted, pss...)
		} else if ev.isChangedDir {
			changedPaths.changedDirs = append(changedPaths.changedDirs, pss...)
		} else if ev.added {
			changedPaths.addedFiles = append(changedPaths.addedFiles, pss...)
		} else {
			changedPaths.changedFiles = append(changedPaths.changedFiles, pss...)
		}
	}

	// Find the most specific identity possible.
	handleChange := func(pathInfo *paths.Path, delete, add, isDir bool) {
		switch pathInfo.Component() {
		case files.ComponentFolderContent:
			logger.Println("Source changed", pathInfo.Path())
			isContentDataFile := pathInfo.IsContentData()
			if !isContentDataFile {
				if ids := h.pageTrees.collectAndMarkStaleIdentities(pathInfo); len(ids) > 0 {
					changes = append(changes, ids...)
				}
			} else {
				h.pageTrees.treePagesFromTemplateAdapters.DeleteAllFunc(pathInfo.Base(),
					func(s string, n *pagesfromdata.PagesFromTemplate) bool {
						changes = append(changes, n.DependencyManager)

						// Try to open the file to see if has been deleted.
						f, err := n.GoTmplFi.Meta().Open()
						if err == nil {
							f.Close()
						}

						if err != nil {
							// Remove all pages and resources below.
							prefix := paths.AddTrailingSlash(pathInfo.Base())

							h.pageTrees.treePages.DeletePrefixRaw(prefix)
							h.pageTrees.resourceTrees.DeletePrefixRaw(prefix)
							changes = append(changes, hglob.NewGlobIdentity(prefix+"**"))
						}
						return err != nil
					})
			}

			needsPagesAssemble = true

			if config.RecentlyTouched != nil {
				// Fast render mode. Adding them to the visited queue
				// avoids rerendering them on navigation.
				for _, id := range changes {
					if p, ok := id.(page.Page); ok {
						config.RecentlyTouched.Add(p.RelPermalink())
					}
				}
			}

			h.pageTrees.treeTaxonomyEntries.DeletePrefix("")

			if delete && !isContentDataFile {
				_, ok := h.pageTrees.treePages.LongestPrefixRaw(pathInfo.Base())
				if ok {
					h.pageTrees.treePages.DeletePrefixRaw(pathInfo.Base())
					h.pageTrees.resourceTrees.DeletePrefixRaw(pathInfo.Base())
					if pathInfo.IsBundle() {
						// Assume directory removed.
						h.pageTrees.treePages.DeletePrefixRaw(pathInfo.Base() + "/")
						h.pageTrees.resourceTrees.DeletePrefixRaw(pathInfo.Base() + "/")
					}
				} else {
					h.pageTrees.resourceTrees.DeletePrefixRaw(pathInfo.Base())
				}
			}

			structural := delete
			structural = structural || (add && pathInfo.IsLeafBundle())

			addedOrChangedContent = append(addedOrChangedContent, pathChange{p: pathInfo, structural: structural, isDir: isDir})

		case files.ComponentFolderLayouts:
			tmplChanged = true
			templatePath := pathInfo.Unnormalized().TrimLeadingSlash().PathNoLang()
			if !h.GetTemplateStore().HasTemplate(templatePath) {
				tmplAdded = true
			}

			if tmplAdded {
				logger.Println("Template added", pathInfo.Path())
				// A new template may require a more coarse grained build.
				base := pathInfo.Base()
				if strings.Contains(base, "_markup") {
					// It's hard to determine the exact change set of this,
					// so be very coarse grained.
					changes = append(changes, identity.GenghisKhan)
				}
				if strings.Contains(base, "shortcodes") {
					// Add both the shortcode file itself (for template refresh) and a glob for dependent content
					changes = append(changes, pathInfo)
					changes = append(changes, hglob.NewGlobIdentity(fmt.Sprintf("/_shortcodes/%s*", pathInfo.BaseNameNoIdentifier())))
				} else {
					changes = append(changes, pathInfo)
				}
			} else {
				logger.Println("Template changed", pathInfo.Path())
				id := h.GetTemplateStore().GetIdentity(pathInfo.Path())
				if id != nil {
					changes = append(changes, id)
				} else {
					changes = append(changes, pathInfo)
				}
			}
		case files.ComponentFolderAssets:
			logger.Println("Asset changed", pathInfo.Path())
			changes = append(changes, pathInfo)
		case files.ComponentFolderData:
			logger.Println("Data changed", pathInfo.Path())

			// This should cover all usage of hugo.Data.
			// Currently very coarse grained.
			changes = append(changes, siteidentities.Data)
			h.init.data.Reset()
		case files.ComponentFolderI18n:
			logger.Println("i18n changed", pathInfo.Path())
			i18nChanged = true
			// It's hard to determine the exact change set of this,
			// so be very coarse grained for now.
			changes = append(changes, identity.GenghisKhan)
		case files.ComponentFolderArchetypes:
			// Ignore for now.
		default:
			panic(fmt.Sprintf("unknown component: %q", pathInfo.Component()))
		}
	}

	changedPaths.deleted = removeDuplicatePaths(changedPaths.deleted)
	changedPaths.addedFiles = removeDuplicatePaths(changedPaths.addedFiles)
	changedPaths.changedFiles = removeDuplicatePaths(changedPaths.changedFiles)

	h.Log.Trace(logg.StringFunc(func() string {
		var sb strings.Builder
		sb.WriteString("Resolved paths:\n")
		sb.WriteString("Deleted:\n")
		for _, p := range changedPaths.deleted {
			sb.WriteString("path: " + p.Path())
			sb.WriteString("\n")
		}
		sb.WriteString("Added:\n")
		for _, p := range changedPaths.addedFiles {
			sb.WriteString("path: " + p.Path())
			sb.WriteString("\n")
		}
		sb.WriteString("Changed:\n")
		for _, p := range changedPaths.changedFiles {
			sb.WriteString("path: " + p.Path())
			sb.WriteString("\n")
		}
		return sb.String()
	}))

	for _, deletedDir := range deletedDirs {
		prefix := deletedDir + "/"
		predicate := func(id identity.Identity) bool {
			// This will effectively reset all pages below this dir.
			return strings.HasPrefix(paths.AddLeadingSlash(id.IdentifierBase()), prefix)
		}
		// Test in both directions.
		changes = append(changes, identity.NewPredicateIdentity(
			// Is dependent.
			predicate,
			// Is dependency.
			predicate,
		),
		)
	}

	if len(addedContentPaths) > 0 {
		// These content files are new and not in use anywhere.
		// To make sure that these gets listed in any site.RegularPages ranges or similar
		// we could invalidate everything, but first try to collect a sample set
		// from the surrounding pages.
		var surroundingIDs []identity.Identity
		for _, p := range addedContentPaths {
			if ids := h.pageTrees.collectIdentitiesSurrounding(p.Base(), 10); len(ids) > 0 {
				surroundingIDs = append(surroundingIDs, ids...)
			}
		}

		if len(surroundingIDs) > 0 {
			changes = append(changes, surroundingIDs...)
		} else {
			// No surrounding pages found, so invalidate everything.
			changes = append(changes, identity.GenghisKhan)
		}
	}

	for _, deleted := range changedPaths.deleted {
		handleChange(deleted, true, false, false)
	}

	for _, id := range changedPaths.addedFiles {
		handleChange(id, false, true, false)
	}

	for _, id := range changedPaths.changedFiles {
		handleChange(id, false, false, false)
	}

	for _, id := range changedPaths.changedDirs {
		handleChange(id, false, false, true)
	}

	for _, id := range changes {
		if id == identity.GenghisKhan {
			for i, cp := range addedOrChangedContent {
				cp.structural = true
				addedOrChangedContent[i] = cp
			}
			break
		}
	}

	resourceFiles := h.fileEventsContentPaths(addedOrChangedContent)

	changed := &WhatChanged{
		needsPagesAssembly: needsPagesAssemble,
	}
	changed.Add(changes...)

	config.WhatChanged = changed

	if err := init(config); err != nil {
		return err
	}

	var cacheBusterOr func(string) bool
	if len(cacheBusters) > 0 {
		cacheBusterOr = func(s string) bool {
			for _, cb := range cacheBusters {
				if cb(s) {
					return true
				}
			}
			return false
		}
	}

	changes2 := changed.Changes()
	h.Deps.OnChangeListeners.Notify(changes2...)

	if err := h.resolveAndClearStateForIdentities(ctx, l, cacheBusterOr, changed.Drain()); err != nil {
		return err
	}

	if tmplChanged {
		if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
			depsFinder := identity.NewFinder(identity.FinderConfig{})
			ll := l.WithField("substep", "rebuild templates")
			s := h.Sites[0]
			if err := s.Deps.TemplateStore.RefreshFiles(func(fi hugofs.FileMetaInfo) bool {
				pi := fi.Meta().PathInfo
				for _, id := range changes2 {
					if depsFinder.Contains(pi, id, -1) > 0 {
						return true
					}
				}
				return false
			}); err != nil {
				return ll, err
			}

			return ll, nil
		}); err != nil {
			return err
		}
	}

	if i18nChanged {
		if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
			ll := l.WithField("substep", "rebuild i18n")
			var prototype *deps.Deps
			for i, s := range h.Sites {
				if err := s.Deps.Compile(prototype); err != nil {
					return ll, err
				}
				if i == 0 {
					prototype = s.Deps
				}
			}
			return ll, nil
		}); err != nil {
			return err
		}
	}

	if resourceFiles != nil {
		if err := h.processFiles(ctx, l, config, resourceFiles...); err != nil {
			return err
		}
	}

	if h.isRebuild() {
		if err := h.processContentAdaptersOnRebuild(ctx, config); err != nil {
			return err
		}
	}

	return nil
}

func (h *HugoSites) LogServerAddresses() {
	if h.hugoInfo.IsMultihost() {
		for _, s := range h.Sites {
			h.Log.Printf("Web Server is available at %s (bind address %s) %s\n", s.conf.C.BaseURL, s.conf.C.ServerInterface, s.Language().Lang)
		}
	} else {
		s := h.Sites[0]
		h.Log.Printf("Web Server is available at %s (bind address %s)\n", s.conf.C.BaseURL, s.conf.C.ServerInterface)
	}
}

func (h *HugoSites) processFull(ctx context.Context, l logg.LevelLogger, config *BuildCfg) (err error) {
	if err = h.processFiles(ctx, l, config); err != nil {
		err = fmt.Errorf("readAndProcessContent: %w", err)
		return
	}
	return err
}

func (s *Site) handleContentAdapterChanges(bi pagesfromdata.BuildInfo, buildConfig *BuildCfg) {
	if !s.h.isRebuild() {
		return
	}

	if len(bi.ChangedIdentities) > 0 {
		buildConfig.WhatChanged.Add(bi.ChangedIdentities...)
		buildConfig.WhatChanged.needsPagesAssembly = true
	}

	for _, p := range bi.DeletedPaths {
		pp := paths.AddLeadingSlash(path.Join(bi.Path.Base(), p.Path))
		df := func(n contentNode) bool {
			if len(p.Hashes) == 0 {
				return true
			}
			if nn, ok := n.(contentNodeSourceEntryIDProvider); ok {
				if i, ok := nn.nodeSourceEntryID().(uint64); ok && i != 0 {
					if _, found := p.Hashes[i]; found {
						return true
					}
				}
			}
			return false
		}

		if v, count := s.pageMap.treePages.DeleteFuncRaw(pp, df); count > 0 {
			buildConfig.WhatChanged.Add(cnh.GetIdentity(v))
		}
		if v, count := s.pageMap.treeResources.DeleteFuncRaw(pp, df); count > 0 {
			buildConfig.WhatChanged.Add(cnh.GetIdentity(v))

			// A deleted resource may affect its parent page.
			if _, v := s.pageMap.treePages.LongestPrefiValueRaw(pp); v != nil {
				buildConfig.WhatChanged.Add(cnh.GetIdentity(v))
			}
		}
	}
}

func (h *HugoSites) processContentAdaptersOnRebuild(ctx context.Context, buildConfig *BuildCfg) error {
	g := rungroup.Run(ctx, rungroup.Config[*pagesfromdata.PagesFromTemplate]{
		NumWorkers: h.numWorkers,
		Handle: func(ctx context.Context, p *pagesfromdata.PagesFromTemplate) error {
			bi, err := p.Execute(ctx)
			if err != nil {
				return err
			}
			s := p.Site.(*Site)
			s.handleContentAdapterChanges(bi, buildConfig)
			return nil
		},
	})

	h.pageTrees.treePagesFromTemplateAdapters.WalkPrefixRaw(doctree.LockTypeRead, "", func(key string, p *pagesfromdata.PagesFromTemplate) (bool, error) {
		if p.StaleVersion() > 0 {
			g.Enqueue(p)
		}
		return false, nil
	})

	return g.Wait()
}

func (s *HugoSites) processFiles(ctx context.Context, l logg.LevelLogger, buildConfig *BuildCfg, filenames ...pathChange) error {
	if s.Deps == nil {
		panic("nil deps on site")
	}

	sourceSpec := source.NewSourceSpec(s.PathSpec, buildConfig.ContentInclusionFilter, s.BaseFs.Content.Fs)

	// For inserts, we can pick an arbitrary pageMap.
	pageMap := s.Sites[0].pageMap

	c := newPagesCollector(ctx, s.h, sourceSpec, s.Log, l, pageMap, buildConfig, filenames)

	if err := c.Collect(); err != nil {
		return err
	}

	return nil
}
