// Copyright 2023 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 commands

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"runtime/pprof"
	"runtime/trace"
	"strings"
	"sync"
	"time"

	"github.com/bep/simplecobra"
	"github.com/fsnotify/fsnotify"
	"github.com/gohugoio/hugo/common/herrors"
	"github.com/gohugoio/hugo/common/htime"
	"github.com/gohugoio/hugo/common/hugo"
	"github.com/gohugoio/hugo/common/maps"
	"github.com/gohugoio/hugo/common/terminal"
	"github.com/gohugoio/hugo/common/types"
	"github.com/gohugoio/hugo/config"
	"github.com/gohugoio/hugo/helpers"
	"github.com/gohugoio/hugo/hugofs"
	"github.com/gohugoio/hugo/hugolib"
	"github.com/gohugoio/hugo/hugolib/filesystems"
	"github.com/gohugoio/hugo/livereload"
	"github.com/gohugoio/hugo/resources/page"
	"github.com/gohugoio/hugo/tpl"
	"github.com/gohugoio/hugo/watcher"
	"github.com/spf13/afero"
	"github.com/spf13/fsync"
	"golang.org/x/sync/errgroup"
	"golang.org/x/sync/semaphore"
)

type hugoBuilder struct {
	r *rootCommand

	confmu sync.Mutex
	conf   *commonConfig

	// May be nil.
	s *serverCommand

	// Currently only set when in "fast render mode".
	changeDetector *fileChangeDetector
	visitedURLs    *types.EvictingStringQueue

	fullRebuildSem *semaphore.Weighted
	debounce       func(f func())

	onConfigLoaded func(reloaded bool) error

	fastRenderMode     bool
	buildWatch         bool
	showErrorInBrowser bool

	errState hugoBuilderErrState
}

func (c *hugoBuilder) withConfE(fn func(conf *commonConfig) error) error {
	c.confmu.Lock()
	defer c.confmu.Unlock()
	return fn(c.conf)
}

func (c *hugoBuilder) withConf(fn func(conf *commonConfig)) {
	c.confmu.Lock()
	defer c.confmu.Unlock()
	fn(c.conf)

}

type hugoBuilderErrState struct {
	mu       sync.Mutex
	paused   bool
	builderr error
	waserr   bool
}

func (e *hugoBuilderErrState) setPaused(p bool) {
	e.mu.Lock()
	defer e.mu.Unlock()
	e.paused = p
}

func (e *hugoBuilderErrState) isPaused() bool {
	e.mu.Lock()
	defer e.mu.Unlock()
	return e.paused
}

func (e *hugoBuilderErrState) setBuildErr(err error) {
	e.mu.Lock()
	defer e.mu.Unlock()
	e.builderr = err
}

func (e *hugoBuilderErrState) buildErr() error {
	e.mu.Lock()
	defer e.mu.Unlock()
	return e.builderr
}

func (e *hugoBuilderErrState) setWasErr(w bool) {
	e.mu.Lock()
	defer e.mu.Unlock()
	e.waserr = w
}

func (e *hugoBuilderErrState) wasErr() bool {
	e.mu.Lock()
	defer e.mu.Unlock()
	return e.waserr
}

func (c *hugoBuilder) errCount() int {
	return int(c.r.logger.LogCounters().ErrorCounter.Count())
}

// getDirList provides NewWatcher() with a list of directories to watch for changes.
func (c *hugoBuilder) getDirList() ([]string, error) {
	var filenames []string

	walkFn := func(path string, fi hugofs.FileMetaInfo, err error) error {
		if err != nil {
			c.r.logger.Errorln("walker: ", err)
			return nil
		}

		if fi.IsDir() {
			if fi.Name() == ".git" ||
				fi.Name() == "node_modules" || fi.Name() == "bower_components" {
				return filepath.SkipDir
			}

			filenames = append(filenames, fi.Meta().Filename)
		}

		return nil
	}

	h, err := c.hugo()
	if err != nil {
		return nil, err
	}
	watchFiles := h.PathSpec.BaseFs.WatchDirs()
	for _, fi := range watchFiles {
		if !fi.IsDir() {
			filenames = append(filenames, fi.Meta().Filename)
			continue
		}

		w := hugofs.NewWalkway(hugofs.WalkwayConfig{Logger: c.r.logger, Info: fi, WalkFn: walkFn})
		if err := w.Walk(); err != nil {
			c.r.logger.Errorln("walker: ", err)
		}
	}

	filenames = helpers.UniqueStringsSorted(filenames)

	return filenames, nil
}

func (c *hugoBuilder) initCPUProfile() (func(), error) {
	if c.r.cpuprofile == "" {
		return nil, nil
	}

	f, err := os.Create(c.r.cpuprofile)
	if err != nil {
		return nil, fmt.Errorf("failed to create CPU profile: %w", err)
	}
	if err := pprof.StartCPUProfile(f); err != nil {
		return nil, fmt.Errorf("failed to start CPU profile: %w", err)
	}
	return func() {
		pprof.StopCPUProfile()
		f.Close()
	}, nil
}

func (c *hugoBuilder) initMemProfile() {
	if c.r.memprofile == "" {
		return
	}

	f, err := os.Create(c.r.memprofile)
	if err != nil {
		c.r.logger.Errorf("could not create memory profile: ", err)
	}
	defer f.Close()
	runtime.GC() // get up-to-date statistics
	if err := pprof.WriteHeapProfile(f); err != nil {
		c.r.logger.Errorf("could not write memory profile: ", err)
	}
}

func (c *hugoBuilder) initMemTicker() func() {
	memticker := time.NewTicker(5 * time.Second)
	quit := make(chan struct{})
	printMem := func() {
		var m runtime.MemStats
		runtime.ReadMemStats(&m)
		fmt.Printf("\n\nAlloc = %v\nTotalAlloc = %v\nSys = %v\nNumGC = %v\n\n", formatByteCount(m.Alloc), formatByteCount(m.TotalAlloc), formatByteCount(m.Sys), m.NumGC)
	}

	go func() {
		for {
			select {
			case <-memticker.C:
				printMem()
			case <-quit:
				memticker.Stop()
				printMem()
				return
			}
		}
	}()

	return func() {
		close(quit)
	}
}

func (c *hugoBuilder) initMutexProfile() (func(), error) {
	if c.r.mutexprofile == "" {
		return nil, nil
	}

	f, err := os.Create(c.r.mutexprofile)
	if err != nil {
		return nil, err
	}

	runtime.SetMutexProfileFraction(1)

	return func() {
		pprof.Lookup("mutex").WriteTo(f, 0)
		f.Close()
	}, nil
}

func (c *hugoBuilder) initProfiling() (func(), error) {
	stopCPUProf, err := c.initCPUProfile()
	if err != nil {
		return nil, err
	}

	stopMutexProf, err := c.initMutexProfile()
	if err != nil {
		return nil, err
	}

	stopTraceProf, err := c.initTraceProfile()
	if err != nil {
		return nil, err
	}

	var stopMemTicker func()
	if c.r.printm {
		stopMemTicker = c.initMemTicker()
	}

	return func() {
		c.initMemProfile()

		if stopCPUProf != nil {
			stopCPUProf()
		}
		if stopMutexProf != nil {
			stopMutexProf()
		}

		if stopTraceProf != nil {
			stopTraceProf()
		}

		if stopMemTicker != nil {
			stopMemTicker()
		}
	}, nil
}

func (c *hugoBuilder) initTraceProfile() (func(), error) {
	if c.r.traceprofile == "" {
		return nil, nil
	}

	f, err := os.Create(c.r.traceprofile)
	if err != nil {
		return nil, fmt.Errorf("failed to create trace file: %w", err)
	}

	if err := trace.Start(f); err != nil {
		return nil, fmt.Errorf("failed to start trace: %w", err)
	}

	return func() {
		trace.Stop()
		f.Close()
	}, nil
}

// newWatcher creates a new watcher to watch filesystem events.
func (c *hugoBuilder) newWatcher(pollIntervalStr string, dirList ...string) (*watcher.Batcher, error) {
	staticSyncer := &staticSyncer{c: c}

	var pollInterval time.Duration
	poll := pollIntervalStr != ""
	if poll {
		pollInterval, err := types.ToDurationE(pollIntervalStr)
		if err != nil {
			return nil, fmt.Errorf("invalid value for flag poll: %s", err)
		}
		c.r.logger.Printf("Use watcher with poll interval %v", pollInterval)
	}

	if pollInterval == 0 {
		pollInterval = 500 * time.Millisecond
	}

	watcher, err := watcher.New(500*time.Millisecond, pollInterval, poll)
	if err != nil {
		return nil, err
	}

	h, err := c.hugo()
	if err != nil {
		return nil, err
	}
	spec := h.Deps.SourceSpec

	for _, d := range dirList {
		if d != "" {
			if spec.IgnoreFile(d) {
				continue
			}
			_ = watcher.Add(d)
		}
	}

	// Identifies changes to config (config.toml) files.
	configSet := make(map[string]bool)
	var configFiles []string
	c.withConf(func(conf *commonConfig) {
		configFiles = conf.configs.LoadingInfo.ConfigFiles
	})

	c.r.logger.Println("Watching for config changes in", strings.Join(configFiles, ", "))
	for _, configFile := range configFiles {
		watcher.Add(configFile)
		configSet[configFile] = true
	}

	go func() {
		for {
			select {
			case evs := <-watcher.Events:
				unlock, err := h.LockBuild()
				if err != nil {
					c.r.logger.Errorln("Failed to acquire a build lock: %s", err)
					return
				}
				c.handleEvents(watcher, staticSyncer, evs, configSet)
				if c.showErrorInBrowser && c.errCount() > 0 {
					// Need to reload browser to show the error
					livereload.ForceRefresh()
				}
				unlock()
			case err := <-watcher.Errors():
				if err != nil && !herrors.IsNotExist(err) {
					c.r.logger.Errorln("Error while watching:", err)
				}
			}
		}
	}()

	return watcher, nil
}

func (c *hugoBuilder) build() error {
	stopProfiling, err := c.initProfiling()
	if err != nil {
		return err
	}

	defer func() {
		if stopProfiling != nil {
			stopProfiling()
		}
	}()

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

	if !c.r.quiet {
		c.r.Println()
		h, err := c.hugo()
		if err != nil {
			return err
		}

		if c.r.printPathWarnings {
			hugofs.WalkFilesystems(h.Fs.PublishDir, func(fs afero.Fs) bool {
				if dfs, ok := fs.(hugofs.DuplicatesReporter); ok {
					dupes := dfs.ReportDuplicates()
					if dupes != "" {
						c.r.logger.Warnln("Duplicate target paths:", dupes)
					}
				}
				return false
			})
		}

		if c.r.printUnusedTemplates {
			unusedTemplates := h.Tmpl().(tpl.UnusedTemplatesProvider).UnusedTemplates()
			for _, unusedTemplate := range unusedTemplates {
				c.r.logger.Warnf("Template %s is unused, source file %s", unusedTemplate.Name(), unusedTemplate.Filename())
			}
		}

		h.PrintProcessingStats(os.Stdout)
		c.r.Println()
	}

	return nil
}

func (c *hugoBuilder) buildSites(noBuildLock bool) (err error) {
	h, err := c.hugo()
	if err != nil {
		return err
	}
	return h.Build(hugolib.BuildCfg{NoBuildLock: noBuildLock})
}

func (c *hugoBuilder) copyStatic() (map[string]uint64, error) {
	m, err := c.doWithPublishDirs(c.copyStaticTo)
	if err == nil || herrors.IsNotExist(err) {
		return m, nil
	}
	return m, err
}

func (c *hugoBuilder) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint64, error) {
	publishDir := helpers.FilePathSeparator

	if sourceFs.PublishFolder != "" {
		publishDir = filepath.Join(publishDir, sourceFs.PublishFolder)
	}

	fs := &countingStatFs{Fs: sourceFs.Fs}

	syncer := fsync.NewSyncer()
	c.withConf(func(conf *commonConfig) {
		syncer.NoTimes = conf.configs.Base.NoTimes
		syncer.NoChmod = conf.configs.Base.NoChmod
		syncer.ChmodFilter = chmodFilter

		syncer.DestFs = conf.fs.PublishDirStatic
		// Now that we are using a unionFs for the static directories
		// We can effectively clean the publishDir on initial sync
		syncer.Delete = conf.configs.Base.CleanDestinationDir
	})

	syncer.SrcFs = fs

	if syncer.Delete {
		c.r.logger.Infoln("removing all files from destination that don't exist in static dirs")

		syncer.DeleteFilter = func(f os.FileInfo) bool {
			return f.IsDir() && strings.HasPrefix(f.Name(), ".")
		}
	}
	c.r.logger.Infoln("syncing static files to", publishDir)

	// because we are using a baseFs (to get the union right).
	// set sync src to root
	err := syncer.Sync(publishDir, helpers.FilePathSeparator)
	if err != nil {
		return 0, err
	}

	// Sync runs Stat 3 times for every source file (which sounds much)
	numFiles := fs.statCounter / 3

	return numFiles, err
}

func (c *hugoBuilder) doWithPublishDirs(f func(sourceFs *filesystems.SourceFilesystem) (uint64, error)) (map[string]uint64, error) {
	langCount := make(map[string]uint64)

	h, err := c.hugo()
	if err != nil {
		return nil, err
	}
	staticFilesystems := h.BaseFs.SourceFilesystems.Static

	if len(staticFilesystems) == 0 {
		c.r.logger.Infoln("No static directories found to sync")
		return langCount, nil
	}

	for lang, fs := range staticFilesystems {
		cnt, err := f(fs)
		if err != nil {
			return langCount, err
		}
		if lang == "" {
			// Not multihost
			c.withConf(func(conf *commonConfig) {
				for _, l := range conf.configs.Languages {
					langCount[l.Lang] = cnt
				}
			})
		} else {
			langCount[lang] = cnt
		}
	}

	return langCount, nil
}

func (c *hugoBuilder) fullBuild(noBuildLock bool) error {
	var (
		g         errgroup.Group
		langCount map[string]uint64
	)

	if !c.r.quiet {
		fmt.Println("Start building sites … ")
		fmt.Println(hugo.BuildVersionString())
		if terminal.IsTerminal(os.Stdout) {
			defer func() {
				fmt.Print(showCursor + clearLine)
			}()
		}
	}

	copyStaticFunc := func() error {
		cnt, err := c.copyStatic()
		if err != nil {
			return fmt.Errorf("error copying static files: %w", err)
		}
		langCount = cnt
		return nil
	}
	buildSitesFunc := func() error {
		if err := c.buildSites(noBuildLock); err != nil {
			return fmt.Errorf("error building site: %w", err)
		}
		return nil
	}
	// Do not copy static files and build sites in parallel if cleanDestinationDir is enabled.
	// This flag deletes all static resources in /public folder that are missing in /static,
	// and it does so at the end of copyStatic() call.
	var cleanDestinationDir bool
	c.withConf(func(conf *commonConfig) {
		cleanDestinationDir = conf.configs.Base.CleanDestinationDir
	})
	if cleanDestinationDir {
		if err := copyStaticFunc(); err != nil {
			return err
		}
		if err := buildSitesFunc(); err != nil {
			return err
		}
	} else {
		g.Go(copyStaticFunc)
		g.Go(buildSitesFunc)
		if err := g.Wait(); err != nil {
			return err
		}
	}

	h, err := c.hugo()
	if err != nil {
		return err
	}
	for _, s := range h.Sites {
		s.ProcessingStats.Static = langCount[s.Language().Lang]
	}

	if c.r.gc {
		count, err := h.GC()
		if err != nil {
			return err
		}
		for _, s := range h.Sites {
			// We have no way of knowing what site the garbage belonged to.
			s.ProcessingStats.Cleaned = uint64(count)
		}
	}

	return nil
}

func (c *hugoBuilder) fullRebuild(changeType string) {
	if changeType == configChangeGoMod {
		// go.mod may be changed during the build itself, and
		// we really want to prevent superfluous builds.
		if !c.fullRebuildSem.TryAcquire(1) {
			return
		}
		c.fullRebuildSem.Release(1)
	}

	c.fullRebuildSem.Acquire(context.Background(), 1)

	go func() {
		defer c.fullRebuildSem.Release(1)

		c.printChangeDetected(changeType)

		defer func() {
			// Allow any file system events to arrive basimplecobra.
			// This will block any rebuild on config changes for the
			// duration of the sleep.
			time.Sleep(2 * time.Second)
		}()

		defer c.r.timeTrack(time.Now(), "Rebuilt")

		err := c.reloadConfig()
		if err != nil {
			// Set the processing on pause until the state is recovered.
			c.errState.setPaused(true)
			c.handleBuildErr(err, "Failed to reload config")
		} else {
			c.errState.setPaused(false)
		}

		if !c.errState.isPaused() {
			_, err := c.copyStatic()
			if err != nil {
				c.r.logger.Errorln(err)
				return
			}
			err = c.buildSites(false)
			if err != nil {
				c.r.logger.Errorln(err)
			} else if c.s != nil && c.s.doLiveReload {
				livereload.ForceRefresh()
			}
		}
	}()
}

func (c *hugoBuilder) handleBuildErr(err error, msg string) {
	c.errState.setBuildErr(err)
	c.r.logger.Errorln(msg + ": " + cleanErrorLog(err.Error()))
}

func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
	staticSyncer *staticSyncer,
	evs []fsnotify.Event,
	configSet map[string]bool) {
	defer func() {
		c.errState.setWasErr(false)
	}()

	var isHandled bool

	for _, ev := range evs {
		isConfig := configSet[ev.Name]
		configChangeType := configChangeConfig
		if isConfig {
			if strings.Contains(ev.Name, "go.mod") {
				configChangeType = configChangeGoMod
			}
			if strings.Contains(ev.Name, ".work") {
				configChangeType = configChangeGoWork
			}
		}
		if !isConfig {
			// It may be one of the /config folders
			dirname := filepath.Dir(ev.Name)
			if dirname != "." && configSet[dirname] {
				isConfig = true
			}
		}

		if isConfig {
			isHandled = true

			if ev.Op&fsnotify.Chmod == fsnotify.Chmod {
				continue
			}

			if ev.Op&fsnotify.Remove == fsnotify.Remove || ev.Op&fsnotify.Rename == fsnotify.Rename {
				c.withConf(func(conf *commonConfig) {
					for _, configFile := range conf.configs.LoadingInfo.ConfigFiles {
						counter := 0
						for watcher.Add(configFile) != nil {
							counter++
							if counter >= 100 {
								break
							}
							time.Sleep(100 * time.Millisecond)
						}
					}
				})
			}

			// Config file(s) changed. Need full rebuild.
			c.fullRebuild(configChangeType)

			return
		}
	}

	if isHandled {
		return
	}

	if c.errState.isPaused() {
		// Wait for the server to get into a consistent state before
		// we continue with processing.
		return
	}

	if len(evs) > 50 {
		// This is probably a mass edit of the content dir.
		// Schedule a full rebuild for when it slows down.
		c.debounce(func() {
			c.fullRebuild("")
		})
		return
	}

	c.r.logger.Infoln("Received System Events:", evs)

	staticEvents := []fsnotify.Event{}
	dynamicEvents := []fsnotify.Event{}

	filtered := []fsnotify.Event{}
	h, err := c.hugo()
	if err != nil {
		c.r.logger.Errorln("Error getting the Hugo object:", err)
		return
	}
	for _, ev := range evs {
		if h.ShouldSkipFileChangeEvent(ev) {
			continue
		}
		// Check the most specific first, i.e. files.
		contentMapped := h.ContentChanges.GetSymbolicLinkMappings(ev.Name)
		if len(contentMapped) > 0 {
			for _, mapped := range contentMapped {
				filtered = append(filtered, fsnotify.Event{Name: mapped, Op: ev.Op})
			}
			continue
		}

		// Check for any symbolic directory mapping.

		dir, name := filepath.Split(ev.Name)

		contentMapped = h.ContentChanges.GetSymbolicLinkMappings(dir)

		if len(contentMapped) == 0 {
			filtered = append(filtered, ev)
			continue
		}

		for _, mapped := range contentMapped {
			mappedFilename := filepath.Join(mapped, name)
			filtered = append(filtered, fsnotify.Event{Name: mappedFilename, Op: ev.Op})
		}
	}

	evs = filtered

	for _, ev := range evs {
		ext := filepath.Ext(ev.Name)
		baseName := filepath.Base(ev.Name)
		istemp := strings.HasSuffix(ext, "~") ||
			(ext == ".swp") || // vim
			(ext == ".swx") || // vim
			(ext == ".tmp") || // generic temp file
			(ext == ".DS_Store") || // OSX Thumbnail
			baseName == "4913" || // vim
			strings.HasPrefix(ext, ".goutputstream") || // gnome
			strings.HasSuffix(ext, "jb_old___") || // intelliJ
			strings.HasSuffix(ext, "jb_tmp___") || // intelliJ
			strings.HasSuffix(ext, "jb_bak___") || // intelliJ
			strings.HasPrefix(ext, ".sb-") || // byword
			strings.HasPrefix(baseName, ".#") || // emacs
			strings.HasPrefix(baseName, "#") // emacs
		if istemp {
			continue
		}
		if h.Deps.SourceSpec.IgnoreFile(ev.Name) {
			continue
		}
		// Sometimes during rm -rf operations a '"": REMOVE' is triggered. Just ignore these
		if ev.Name == "" {
			continue
		}

		// Write and rename operations are often followed by CHMOD.
		// There may be valid use cases for rebuilding the site on CHMOD,
		// but that will require more complex logic than this simple conditional.
		// On OS X this seems to be related to Spotlight, see:
		// https://github.com/go-fsnotify/fsnotify/issues/15
		// A workaround is to put your site(s) on the Spotlight exception list,
		// but that may be a little mysterious for most end users.
		// So, for now, we skip reload on CHMOD.
		// We do have to check for WRITE though. On slower laptops a Chmod
		// could be aggregated with other important events, and we still want
		// to rebuild on those
		if ev.Op&(fsnotify.Chmod|fsnotify.Write|fsnotify.Create) == fsnotify.Chmod {
			continue
		}

		walkAdder := func(path string, f hugofs.FileMetaInfo, err error) error {
			if f.IsDir() {
				c.r.logger.Println("adding created directory to watchlist", path)
				if err := watcher.Add(path); err != nil {
					return err
				}
			} else if !staticSyncer.isStatic(h, path) {
				// Hugo's rebuilding logic is entirely file based. When you drop a new folder into
				// /content on OSX, the above logic will handle future watching of those files,
				// but the initial CREATE is lost.
				dynamicEvents = append(dynamicEvents, fsnotify.Event{Name: path, Op: fsnotify.Create})
			}
			return nil
		}

		// recursively add new directories to watch list
		// When mkdir -p is used, only the top directory triggers an event (at least on OSX)
		if ev.Op&fsnotify.Create == fsnotify.Create {
			c.withConf(func(conf *commonConfig) {
				if s, err := conf.fs.Source.Stat(ev.Name); err == nil && s.Mode().IsDir() {
					_ = helpers.SymbolicWalk(conf.fs.Source, ev.Name, walkAdder)
				}
			})
		}

		if staticSyncer.isStatic(h, ev.Name) {
			staticEvents = append(staticEvents, ev)
		} else {
			dynamicEvents = append(dynamicEvents, ev)
		}
	}

	if len(staticEvents) > 0 {
		c.printChangeDetected("Static files")

		if c.r.forceSyncStatic {
			c.r.logger.Printf("Syncing all static files\n")
			_, err := c.copyStatic()
			if err != nil {
				c.r.logger.Errorln("Error copying static files to publish dir:", err)
				return
			}
		} else {
			if err := staticSyncer.syncsStaticEvents(staticEvents); err != nil {
				c.r.logger.Errorln("Error syncing static files to publish dir:", err)
				return
			}
		}

		if c.s != nil && c.s.doLiveReload {
			// Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized

			// force refresh when more than one file
			if !c.errState.wasErr() && len(staticEvents) == 1 {
				ev := staticEvents[0]
				h, err := c.hugo()
				if err != nil {
					c.r.logger.Errorln("Error getting the Hugo object:", err)
					return
				}
				path := h.BaseFs.SourceFilesystems.MakeStaticPathRelative(ev.Name)
				path = h.RelURL(helpers.ToSlashTrimLeading(path), false)

				livereload.RefreshPath(path)
			} else {
				livereload.ForceRefresh()
			}
		}
	}

	if len(dynamicEvents) > 0 {
		partitionedEvents := partitionDynamicEvents(
			h.BaseFs.SourceFilesystems,
			dynamicEvents)

		onePageName := pickOneWriteOrCreatePath(partitionedEvents.ContentEvents)

		c.printChangeDetected("")
		c.changeDetector.PrepareNew()

		func() {
			defer c.r.timeTrack(time.Now(), "Total")
			if err := c.rebuildSites(dynamicEvents); err != nil {
				c.handleBuildErr(err, "Rebuild failed")
			}
		}()

		if c.s != nil && c.s.doLiveReload {
			if len(partitionedEvents.ContentEvents) == 0 && len(partitionedEvents.AssetEvents) > 0 {
				if c.errState.wasErr() {
					livereload.ForceRefresh()
					return
				}
				changed := c.changeDetector.changed()
				if c.changeDetector != nil && len(changed) == 0 {
					// Nothing has changed.
					return
				} else if len(changed) == 1 {
					pathToRefresh := h.PathSpec.RelURL(helpers.ToSlashTrimLeading(changed[0]), false)
					livereload.RefreshPath(pathToRefresh)
				} else {
					livereload.ForceRefresh()
				}
			}

			if len(partitionedEvents.ContentEvents) > 0 {
				navigate := c.s != nil && c.s.navigateToChanged
				// We have fetched the same page above, but it may have
				// changed.
				var p page.Page

				if navigate {
					if onePageName != "" {
						p = h.GetContentPage(onePageName)
					}
				}

				if p != nil {
					livereload.NavigateToPathForPort(p.RelPermalink(), p.Site().ServerPort())
				} else {
					livereload.ForceRefresh()
				}
			}
		}
	}
}

func (c *hugoBuilder) hugo() (*hugolib.HugoSites, error) {
	var h *hugolib.HugoSites
	if err := c.withConfE(func(conf *commonConfig) error {
		var err error
		h, err = c.r.HugFromConfig(conf)
		return err

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

	if c.s != nil {
		// A running server, register the media types.
		for _, s := range h.Sites {
			s.RegisterMediaTypes()
		}
	}
	return h, nil
}

func (c *hugoBuilder) hugoTry() *hugolib.HugoSites {
	var h *hugolib.HugoSites
	c.withConf(func(conf *commonConfig) {
		h, _ = c.r.HugFromConfig(conf)
	})
	return h
}

func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error {
	cfg := config.New()
	cfg.Set("renderToDisk", (c.s == nil && !c.r.renderToMemory) || (c.s != nil && c.s.renderToDisk))
	watch := c.r.buildWatch || (c.s != nil && c.s.serverWatch)
	if c.r.environment == "" {
		// We need to set the environment as early as possible because we need it to load the correct config.
		// Check if the user has set it in env.
		if env := os.Getenv("HUGO_ENVIRONMENT"); env != "" {
			c.r.environment = env
		} else if env := os.Getenv("HUGO_ENV"); env != "" {
			c.r.environment = env
		} else {
			if c.s != nil {
				// The server defaults to development.
				c.r.environment = hugo.EnvironmentDevelopment
			} else {
				c.r.environment = hugo.EnvironmentProduction
			}
		}
	}
	cfg.Set("environment", c.r.environment)

	cfg.Set("internal", maps.Params{
		"running": running,
		"watch":   watch,
		"verbose": c.r.verbose,
	})

	conf, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), flagsToCfg(cd, cfg))
	if err != nil {
		return err
	}

	if len(conf.configs.LoadingInfo.ConfigFiles) == 0 {
		return errors.New("Unable to locate config file or config directory. Perhaps you need to create a new site.\nRun `hugo help new` for details.")
	}

	c.conf = conf
	if c.onConfigLoaded != nil {
		if err := c.onConfigLoaded(false); err != nil {
			return err
		}
	}

	return nil

}

func (c *hugoBuilder) printChangeDetected(typ string) {
	msg := "\nChange"
	if typ != "" {
		msg += " of " + typ
	}
	msg += " detected, rebuilding site."

	c.r.logger.Println(msg)
	const layout = "2006-01-02 15:04:05.000 -0700"
	c.r.logger.Println(htime.Now().Format(layout))
}

func (c *hugoBuilder) rebuildSites(events []fsnotify.Event) error {
	if err := c.errState.buildErr(); err != nil {
		ferrs := herrors.UnwrapFileErrorsWithErrorContext(err)
		for _, err := range ferrs {
			events = append(events, fsnotify.Event{Name: err.Position().Filename, Op: fsnotify.Write})
		}
	}
	c.errState.setBuildErr(nil)
	visited := c.visitedURLs.PeekAllSet()
	h, err := c.hugo()
	if err != nil {
		return err
	}
	if c.fastRenderMode {
		c.withConf(func(conf *commonConfig) {
			// Make sure we always render the home pages
			for _, l := range conf.configs.Languages {
				langPath := h.GetLangSubDir(l.Lang)
				if langPath != "" {
					langPath = langPath + "/"
				}
				home := h.PrependBasePath("/"+langPath, false)
				visited[home] = true
			}
		})
	}
	return h.Build(hugolib.BuildCfg{NoBuildLock: true, RecentlyVisited: visited, ErrRecovery: c.errState.wasErr()}, events...)
}

func (c *hugoBuilder) reloadConfig() error {
	c.r.Reset()
	c.r.configVersionID.Add(1)

	if err := c.withConfE(func(conf *commonConfig) error {
		oldConf := conf
		newConf, err := c.r.ConfigFromConfig(c.r.configVersionID.Load(), conf)
		if err != nil {
			return err
		}
		sameLen := len(oldConf.configs.Languages) == len(newConf.configs.Languages)
		if !sameLen {
			if oldConf.configs.IsMultihost || newConf.configs.IsMultihost {
				return errors.New("multihost change detected, please restart server")
			}
		}
		c.conf = newConf
		return nil
	}); err != nil {
		return err
	}

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

	return nil
}
