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

import (
	"context"
	"fmt"
	"io"
	"path/filepath"

	"github.com/gohugoio/hugo/common/hashing"
	"github.com/gohugoio/hugo/common/maps"
	"github.com/gohugoio/hugo/common/paths"
	"github.com/gohugoio/hugo/helpers"
	"github.com/gohugoio/hugo/hugofs"
	"github.com/gohugoio/hugo/identity"
	"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"
	"github.com/mitchellh/mapstructure"
	"github.com/spf13/cast"
)

type PagesFromDataTemplateContext interface {
	// AddPage adds a new page to the site.
	// The first return value will always be an empty string.
	AddPage(any) (string, error)

	// AddResource adds a new resource to the site.
	// The first return value will always be an empty string.
	AddResource(any) (string, error)

	// The site to which the pages will be added.
	Site() page.Site

	// The same template may be executed multiple times for multiple languages.
	// The Store can be used to store state between these invocations.
	Store() *maps.Scratch

	// By default, the template will be executed for the language
	// defined by the _content.gotmpl file (e.g. its mount definition).
	// This method can be used to activate the template for all languages.
	// The return value will always be an empty string.
	EnableAllLanguages() string
}

var _ PagesFromDataTemplateContext = (*pagesFromDataTemplateContext)(nil)

type pagesFromDataTemplateContext struct {
	p *PagesFromTemplate
}

func (p *pagesFromDataTemplateContext) toPathMap(v any) (string, map[string]any, error) {
	m, err := maps.ToStringMapE(v)
	if err != nil {
		return "", nil, err
	}

	path, err := cast.ToStringE(m["path"])
	if err != nil {
		return "", nil, fmt.Errorf("invalid path %q", path)
	}
	return path, m, nil
}

func (p *pagesFromDataTemplateContext) AddPage(v any) (string, error) {
	path, m, err := p.toPathMap(v)
	if err != nil {
		return "", err
	}

	if !p.p.buildState.checkHasChangedAndSetSourceInfo(path, m) {
		return "", nil
	}

	pd := pagemeta.DefaultPageConfig
	pd.IsFromContentAdapter = true
	pd.ContentAdapterData = m

	// The rest will be handled after the cascade is calculated and applied.
	if err := mapstructure.WeakDecode(pd.ContentAdapterData, &pd.PageConfigEarly); err != nil {
		err = fmt.Errorf("failed to decode page map: %w", err)
		return "", err
	}

	if err := pd.Init(true); err != nil {
		return "", err
	}

	p.p.buildState.NumPagesAdded++

	return "", p.p.HandlePage(p.p, &pd)
}

func (p *pagesFromDataTemplateContext) AddResource(v any) (string, error) {
	path, m, err := p.toPathMap(v)
	if err != nil {
		return "", err
	}

	if !p.p.buildState.checkHasChangedAndSetSourceInfo(path, m) {
		return "", nil
	}

	var rd pagemeta.ResourceConfig
	if err := mapstructure.WeakDecode(m, &rd); err != nil {
		return "", err
	}

	p.p.buildState.NumResourcesAdded++

	if err := rd.Validate(); err != nil {
		return "", err
	}

	return "", p.p.HandleResource(p.p, &rd)
}

func (p *pagesFromDataTemplateContext) Site() page.Site {
	return p.p.Site
}

func (p *pagesFromDataTemplateContext) Store() *maps.Scratch {
	return p.p.store
}

func (p *pagesFromDataTemplateContext) EnableAllLanguages() string {
	p.p.buildState.EnableAllLanguages = true
	return ""
}

func NewPagesFromTemplate(opts PagesFromTemplateOptions) *PagesFromTemplate {
	return &PagesFromTemplate{
		PagesFromTemplateOptions: opts,
		PagesFromTemplateDeps:    opts.DepsFromSite(opts.Site),
		buildState: &BuildState{
			sourceInfosCurrent: maps.NewCache[string, *sourceInfo](),
		},
		store: maps.NewScratch(),
	}
}

type PagesFromTemplateOptions struct {
	Site         page.Site
	DepsFromSite func(page.Site) PagesFromTemplateDeps

	DependencyManager identity.Manager

	Watching bool

	HandlePage     func(pt *PagesFromTemplate, p *pagemeta.PageConfig) error
	HandleResource func(pt *PagesFromTemplate, p *pagemeta.ResourceConfig) error

	GoTmplFi hugofs.FileMetaInfo
}

type PagesFromTemplateDeps struct {
	TemplateStore *tplimpl.TemplateStore
}

var _ resource.Staler = (*PagesFromTemplate)(nil)

type PagesFromTemplate struct {
	PagesFromTemplateOptions
	PagesFromTemplateDeps
	buildState *BuildState
	store      *maps.Scratch
}

func (b *PagesFromTemplate) AddChange(id identity.Identity) {
	b.buildState.ChangedIdentities = append(b.buildState.ChangedIdentities, id)
}

func (b *PagesFromTemplate) MarkStale() {
	b.buildState.StaleVersion++
}

func (b *PagesFromTemplate) StaleVersion() uint32 {
	return b.buildState.StaleVersion
}

type BuildInfo struct {
	NumPagesAdded      uint64
	NumResourcesAdded  uint64
	EnableAllLanguages bool
	ChangedIdentities  []identity.Identity
	DeletedPaths       []string
	Path               *paths.Path
}

type BuildState struct {
	StaleVersion uint32

	EnableAllLanguages bool

	// Paths deleted in the current build.
	DeletedPaths []string

	// Changed identities in the current build.
	ChangedIdentities []identity.Identity

	NumPagesAdded     uint64
	NumResourcesAdded uint64

	sourceInfosCurrent  *maps.Cache[string, *sourceInfo]
	sourceInfosPrevious *maps.Cache[string, *sourceInfo]
}

func (b *BuildState) hash(v any) uint64 {
	return hashing.HashUint64(v)
}

func (b *BuildState) checkHasChangedAndSetSourceInfo(changedPath string, v any) bool {
	h := b.hash(v)
	si, found := b.sourceInfosPrevious.Get(changedPath)
	if found {
		b.sourceInfosCurrent.Set(changedPath, si)
		if si.hash == h {
			return false
		}
	} else {
		si = &sourceInfo{}
		b.sourceInfosCurrent.Set(changedPath, si)
	}
	si.hash = h
	return true
}

func (b *BuildState) resolveDeletedPaths() {
	if b.sourceInfosPrevious == nil {
		b.DeletedPaths = nil
		return
	}
	var paths []string
	b.sourceInfosPrevious.ForEeach(func(k string, _ *sourceInfo) bool {
		if _, found := b.sourceInfosCurrent.Get(k); !found {
			paths = append(paths, k)
		}
		return true
	})

	b.DeletedPaths = paths
}

func (b *BuildState) PrepareNextBuild() {
	b.sourceInfosPrevious = b.sourceInfosCurrent
	b.sourceInfosCurrent = maps.NewCache[string, *sourceInfo]()
	b.StaleVersion = 0
	b.DeletedPaths = nil
	b.ChangedIdentities = nil
	b.NumPagesAdded = 0
	b.NumResourcesAdded = 0
}

type sourceInfo struct {
	hash uint64
}

func (p PagesFromTemplate) CloneForSite(s page.Site) *PagesFromTemplate {
	// We deliberately make them share the same DependencyManager and Store.
	p.PagesFromTemplateOptions.Site = s
	p.PagesFromTemplateDeps = p.PagesFromTemplateOptions.DepsFromSite(s)
	p.buildState = &BuildState{
		sourceInfosCurrent: maps.NewCache[string, *sourceInfo](),
	}
	return &p
}

func (p PagesFromTemplate) CloneForGoTmpl(fi hugofs.FileMetaInfo) *PagesFromTemplate {
	p.PagesFromTemplateOptions.GoTmplFi = fi
	return &p
}

func (p *PagesFromTemplate) GetDependencyManagerForScope(scope int) identity.Manager {
	return p.DependencyManager
}

func (p *PagesFromTemplate) GetDependencyManagerForScopesAll() []identity.Manager {
	return []identity.Manager{p.DependencyManager}
}

func (p *PagesFromTemplate) Execute(ctx context.Context) (BuildInfo, error) {
	defer func() {
		p.buildState.PrepareNextBuild()
	}()

	f, err := p.GoTmplFi.Meta().Open()
	if err != nil {
		return BuildInfo{}, err
	}
	defer f.Close()

	tmpl, err := p.TemplateStore.TextParse(filepath.ToSlash(p.GoTmplFi.Meta().Filename), helpers.ReaderToString(f))
	if err != nil {
		return BuildInfo{}, err
	}

	data := &pagesFromDataTemplateContext{
		p: p,
	}

	ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, p)

	if err := p.TemplateStore.ExecuteWithContext(ctx, tmpl, io.Discard, data); err != nil {
		return BuildInfo{}, err
	}

	if p.Watching {
		p.buildState.resolveDeletedPaths()
	}

	bi := BuildInfo{
		NumPagesAdded:      p.buildState.NumPagesAdded,
		NumResourcesAdded:  p.buildState.NumResourcesAdded,
		EnableAllLanguages: p.buildState.EnableAllLanguages,
		ChangedIdentities:  p.buildState.ChangedIdentities,
		DeletedPaths:       p.buildState.DeletedPaths,
		Path:               p.GoTmplFi.Meta().PathInfo,
	}

	return bi, nil
}
