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

import (
	"context"
	"errors"
	"fmt"
	"html/template"
	"sort"
	"time"

	radix "github.com/armon/go-radix"
	"github.com/gohugoio/hugo/common/hugo"
	"github.com/gohugoio/hugo/common/loggers"
	"github.com/gohugoio/hugo/common/maps"
	"github.com/gohugoio/hugo/common/para"
	"github.com/gohugoio/hugo/config"
	"github.com/gohugoio/hugo/config/allconfig"
	"github.com/gohugoio/hugo/deps"
	"github.com/gohugoio/hugo/identity"
	"github.com/gohugoio/hugo/langs"
	"github.com/gohugoio/hugo/langs/i18n"
	"github.com/gohugoio/hugo/lazy"
	"github.com/gohugoio/hugo/modules"
	"github.com/gohugoio/hugo/navigation"
	"github.com/gohugoio/hugo/output"
	"github.com/gohugoio/hugo/publisher"
	"github.com/gohugoio/hugo/resources/page"
	"github.com/gohugoio/hugo/resources/page/pagemeta"
	"github.com/gohugoio/hugo/resources/resource"
	"github.com/gohugoio/hugo/tpl"
	"github.com/gohugoio/hugo/tpl/tplimpl"
)

var (
	_ page.Site = (*Site)(nil)
)

type Site struct {
	conf     *allconfig.Config
	language *langs.Language

	// The owning container.
	h *HugoSites

	*deps.Deps

	// Page navigation.
	*PageCollections
	taxonomies page.TaxonomyList
	menus      navigation.Menus

	siteBucket *pagesMapBucket

	// Shortcut to the home page. Note that this may be nil if
	// home page, for some odd reason, is disabled.
	home *pageState

	// The last modification date of this site.
	lastmod time.Time

	relatedDocsHandler *page.RelatedDocsHandler
	siteRefLinker
	publisher          publisher.Publisher
	frontmatterHandler pagemeta.FrontMatterHandler

	// We render each site for all the relevant output formats in serial with
	// this rendering context pointing to the current one.
	rc *siteRenderingContext

	// The output formats that we need to render this site in. This slice
	// will be fixed once set.
	// This will be the union of Site.Pages' outputFormats.
	// This slice will be sorted.
	renderFormats output.Formats

	// Lazily loaded site dependencies
	init *siteInit
}

func (s *Site) Debug() {
	fmt.Println("Debugging site", s.Lang(), "=>")
	fmt.Println(s.pageMap.testDump())
}

// NewHugoSites creates HugoSites from the given config.
func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) {
	conf := cfg.Configs.GetFirstLanguageConfig()

	logger := cfg.Logger
	if logger == nil {
		logger = loggers.NewErrorLogger()
	}
	ignorableLogger := loggers.NewIgnorableLogger(logger, conf.IgnoredErrors())

	firstSiteDeps := &deps.Deps{
		Fs:                  cfg.Fs,
		Log:                 ignorableLogger,
		Conf:                conf,
		TemplateProvider:    tplimpl.DefaultTemplateProvider,
		TranslationProvider: i18n.NewTranslationProvider(),
	}
	if err := firstSiteDeps.Init(); err != nil {
		return nil, err
	}

	confm := cfg.Configs
	var sites []*Site

	for i, confp := range confm.ConfigLangs() {
		language := confp.Language()
		if confp.IsLangDisabled(language.Lang) {
			continue
		}
		k := language.Lang
		conf := confm.LanguageConfigMap[k]

		frontmatterHandler, err := pagemeta.NewFrontmatterHandler(cfg.Logger, conf.Frontmatter)
		if err != nil {
			return nil, err
		}

		langs.SetParams(language, conf.Params)

		s := &Site{
			conf:     conf,
			language: language,
			siteBucket: &pagesMapBucket{
				cascade: conf.Cascade.Config,
			},
			frontmatterHandler: frontmatterHandler,
		}

		if i == 0 {
			firstSiteDeps.Site = s
			s.Deps = firstSiteDeps
		} else {
			d, err := firstSiteDeps.Clone(s, confp)
			if err != nil {
				return nil, err
			}
			s.Deps = d
		}

		// Site deps start.
		var taxonomiesConfig taxonomiesConfig = conf.Taxonomies
		pm := &pageMap{
			contentMap: newContentMap(contentMapConfig{
				lang:                 k,
				taxonomyConfig:       taxonomiesConfig.Values(),
				taxonomyDisabled:     !conf.IsKindEnabled(page.KindTerm),
				taxonomyTermDisabled: !conf.IsKindEnabled(page.KindTaxonomy),
				pageDisabled:         !conf.IsKindEnabled(page.KindPage),
			}),
			s: s,
		}

		s.PageCollections = newPageCollections(pm)
		s.siteRefLinker, err = newSiteRefLinker(s)

		// Set up the main publishing chain.
		pub, err := publisher.NewDestinationPublisher(
			firstSiteDeps.ResourceSpec,
			s.conf.OutputFormats.Config,
			s.conf.MediaTypes.Config,
		)
		if err != nil {
			return nil, err
		}

		s.publisher = pub
		s.relatedDocsHandler = page.NewRelatedDocsHandler(s.conf.Related)
		// Site deps end.

		s.prepareInits()
		sites = append(sites, s)
	}

	if len(sites) == 0 {
		return nil, errors.New("no sites to build")
	}

	// Sort the sites by language weight (if set) or lang.
	sort.Slice(sites, func(i, j int) bool {
		li := sites[i].language
		lj := sites[j].language
		if li.Weight != lj.Weight {
			return li.Weight < lj.Weight
		}
		return li.Lang < lj.Lang
	})

	h, err := newHugoSitesNew(cfg, firstSiteDeps, sites)
	if err == nil && h == nil {
		panic("hugo: newHugoSitesNew returned nil error and nil HugoSites")
	}

	return h, err
}

func newHugoSitesNew(cfg deps.DepsCfg, d *deps.Deps, sites []*Site) (*HugoSites, error) {
	numWorkers := config.GetNumWorkerMultiplier()
	if numWorkers > len(sites) {
		numWorkers = len(sites)
	}
	var workers *para.Workers
	if numWorkers > 1 {
		workers = para.New(numWorkers)
	}

	h := &HugoSites{
		Sites:                   sites,
		Deps:                    sites[0].Deps,
		Configs:                 cfg.Configs,
		workers:                 workers,
		numWorkers:              numWorkers,
		currentSite:             sites[0],
		skipRebuildForFilenames: make(map[string]bool),
		init: &hugoSitesInit{
			data:         lazy.New(),
			layouts:      lazy.New(),
			gitInfo:      lazy.New(),
			translations: lazy.New(),
		},
	}

	// Assemble dependencies to be used in hugo.Deps.
	var dependencies []*hugo.Dependency
	var depFromMod func(m modules.Module) *hugo.Dependency
	depFromMod = func(m modules.Module) *hugo.Dependency {
		dep := &hugo.Dependency{
			Path:    m.Path(),
			Version: m.Version(),
			Time:    m.Time(),
			Vendor:  m.Vendor(),
		}

		// These are pointers, but this all came from JSON so there's no recursive navigation,
		// so just create new values.
		if m.Replace() != nil {
			dep.Replace = depFromMod(m.Replace())
		}
		if m.Owner() != nil {
			dep.Owner = depFromMod(m.Owner())
		}
		return dep
	}
	for _, m := range d.Paths.AllModules() {
		dependencies = append(dependencies, depFromMod(m))
	}

	h.hugoInfo = hugo.NewInfo(h.Configs.GetFirstLanguageConfig(), dependencies)

	var prototype *deps.Deps
	for i, s := range sites {
		s.h = h
		if err := s.Deps.Compile(prototype); err != nil {
			return nil, err
		}
		if i == 0 {
			prototype = s.Deps
		}
	}

	h.fatalErrorHandler = &fatalErrorHandler{
		h:     h,
		donec: make(chan bool),
	}

	// Only needed in server mode.
	if cfg.Configs.Base.Internal.Watch {
		h.ContentChanges = &contentChangeMap{
			pathSpec:      h.PathSpec,
			symContent:    make(map[string]map[string]bool),
			leafBundles:   radix.New(),
			branchBundles: make(map[string]bool),
		}
	}

	h.init.data.Add(func(context.Context) (any, error) {
		err := h.loadData(h.PathSpec.BaseFs.Data.Dirs)
		if err != nil {
			return nil, fmt.Errorf("failed to load data: %w", err)
		}
		return nil, nil
	})

	h.init.layouts.Add(func(context.Context) (any, error) {
		for _, s := range h.Sites {
			if err := s.Tmpl().(tpl.TemplateManager).MarkReady(); err != nil {
				return nil, err
			}
		}
		return nil, nil
	})

	h.init.translations.Add(func(context.Context) (any, error) {
		if len(h.Sites) > 1 {
			allTranslations := pagesToTranslationsMap(h.Sites)
			assignTranslationsToPages(allTranslations, h.Sites)
		}

		return nil, nil
	})

	h.init.gitInfo.Add(func(context.Context) (any, error) {
		err := h.loadGitInfo()
		if err != nil {
			return nil, fmt.Errorf("failed to load Git info: %w", err)
		}
		return nil, nil
	})

	return h, nil
}

// Returns true if we're running in a server.
func (s *Site) IsServer() bool {
	return s.conf.Internal.Running
}

// Returns the server port.
func (s *Site) ServerPort() int {
	return s.conf.C.BaseURL.Port()
}

// Returns the configured title for this Site.
func (s *Site) Title() string {
	return s.conf.Title
}

func (s *Site) Copyright() string {
	return s.conf.Copyright
}

func (s *Site) RSSLink() string {
	rssOutputFormat, found := s.conf.C.KindOutputFormats[page.KindHome].GetByName("rss")
	if !found {
		return ""
	}
	return s.permalink(rssOutputFormat.BaseFilename())
}

func (s *Site) Config() page.SiteConfig {
	return page.SiteConfig{
		Privacy:  s.conf.Privacy,
		Services: s.conf.Services,
	}
}

func (s *Site) LanguageCode() string {
	return s.Language().LanguageCode()
}

// Returns all Sites for all languages.
func (s *Site) Sites() page.Sites {
	sites := make(page.Sites, len(s.h.Sites))
	for i, s := range s.h.Sites {
		sites[i] = s.Site()
	}
	return sites
}

// Returns Site currently rendering.
func (s *Site) Current() page.Site {
	return s.h.currentSite
}

// MainSections returns the list of main sections.
func (s *Site) MainSections() []string {
	return s.conf.C.MainSections
}

// Returns a struct with some information about the build.
func (s *Site) Hugo() hugo.HugoInfo {
	if s.h == nil || s.h.hugoInfo.Environment == "" {
		panic("site: hugo: hugoInfo not initialized")
	}
	return s.h.hugoInfo
}

// Returns the BaseURL for this Site.
func (s *Site) BaseURL() template.URL {
	return template.URL(s.conf.C.BaseURL.WithPath)
}

// Returns the last modification date of the content.
func (s *Site) LastChange() time.Time {
	return s.lastmod
}

// Returns the Params configured for this site.
func (s *Site) Params() maps.Params {
	return s.conf.Params
}

func (s *Site) Author() map[string]any {
	return s.conf.Author
}

func (s *Site) Authors() page.AuthorList {
	return page.AuthorList{}
}

func (s *Site) Social() map[string]string {
	return s.conf.Social
}

// TODO(bep): deprecate.
func (s *Site) DisqusShortname() string {
	return s.Config().Services.Disqus.Shortname
}

// TODO(bep): deprecate.
func (s *Site) GoogleAnalytics() string {
	return s.Config().Services.GoogleAnalytics.ID
}

func (s *Site) Param(key any) (any, error) {
	return resource.Param(s, nil, key)
}

// Returns a map of all the data inside /data.
func (s *Site) Data() map[string]any {
	return s.s.h.Data()
}

func (s *Site) BuildDrafts() bool {
	return s.conf.BuildDrafts
}

func (s *Site) IsMultiLingual() bool {
	return s.h.isMultiLingual()
}

func (s *Site) LanguagePrefix() string {
	conf := s.s.Conf
	if !conf.IsMultiLingual() {
		return ""
	}

	if !conf.DefaultContentLanguageInSubdir() && s.language.Lang == conf.DefaultContentLanguage() {
		return ""
	}

	return "/" + s.language.Lang
}

// Returns the identity of this site.
// This is for internal use only.
func (s *Site) GetIdentity() identity.Identity {
	return identity.KeyValueIdentity{Key: "site", Value: s.Lang()}
}

func (s *Site) Site() page.Site {
	return page.WrapSite(s)
}
