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

import (
	"fmt"

	"github.com/gobwas/glob"
	"github.com/gohugoio/hugo/common/maps"
	"github.com/gohugoio/hugo/common/predicate"
	"github.com/gohugoio/hugo/config"
	hglob "github.com/gohugoio/hugo/hugofs/glob"
	"github.com/mitchellh/mapstructure"
)

// Segments is a collection of named segments.
type Segments struct {
	s map[string]excludeInclude
}

type excludeInclude struct {
	exclude predicate.P[SegmentMatcherFields]
	include predicate.P[SegmentMatcherFields]
}

// ShouldExcludeCoarse returns whether the given fields should be excluded.
// This is used for the coarser grained checks, e.g. language and output format.
// Note that ShouldExcludeCoarse(fields) == ShouldExcludeFine(fields) may
// not always be true, but ShouldExcludeCoarse(fields) == true == ShouldExcludeFine(fields)
// will always be truthful.
func (e excludeInclude) ShouldExcludeCoarse(fields SegmentMatcherFields) bool {
	return e.exclude != nil && e.exclude(fields)
}

// ShouldExcludeFine returns whether the given fields should be excluded.
// This is used for the finer grained checks, e.g. on individual pages.
func (e excludeInclude) ShouldExcludeFine(fields SegmentMatcherFields) bool {
	if e.exclude != nil && e.exclude(fields) {
		return true
	}
	return e.include != nil && !e.include(fields)
}

type SegmentFilter interface {
	// ShouldExcludeCoarse returns whether the given fields should be excluded on a coarse level.
	ShouldExcludeCoarse(SegmentMatcherFields) bool

	// ShouldExcludeFine returns whether the given fields should be excluded on a fine level.
	ShouldExcludeFine(SegmentMatcherFields) bool
}

type segmentFilter struct {
	coarse predicate.P[SegmentMatcherFields]
	fine   predicate.P[SegmentMatcherFields]
}

func (f segmentFilter) ShouldExcludeCoarse(field SegmentMatcherFields) bool {
	return f.coarse(field)
}

func (f segmentFilter) ShouldExcludeFine(fields SegmentMatcherFields) bool {
	return f.fine(fields)
}

var (
	matchAll     = func(SegmentMatcherFields) bool { return true }
	matchNothing = func(SegmentMatcherFields) bool { return false }
)

// Get returns a SegmentFilter for the given segments.
func (sms Segments) Get(onNotFound func(s string), ss ...string) SegmentFilter {
	if ss == nil {
		return segmentFilter{coarse: matchNothing, fine: matchNothing}
	}
	var sf segmentFilter
	for _, s := range ss {
		if seg, ok := sms.s[s]; ok {
			if sf.coarse == nil {
				sf.coarse = seg.ShouldExcludeCoarse
			} else {
				sf.coarse = sf.coarse.Or(seg.ShouldExcludeCoarse)
			}
			if sf.fine == nil {
				sf.fine = seg.ShouldExcludeFine
			} else {
				sf.fine = sf.fine.Or(seg.ShouldExcludeFine)
			}
		} else if onNotFound != nil {
			onNotFound(s)
		}
	}

	if sf.coarse == nil {
		sf.coarse = matchAll
	}
	if sf.fine == nil {
		sf.fine = matchAll
	}

	return sf
}

type SegmentConfig struct {
	Excludes []SegmentMatcherFields
	Includes []SegmentMatcherFields
}

// SegmentMatcherFields is a matcher for a segment include or exclude.
// All of these are Glob patterns.
type SegmentMatcherFields struct {
	Kind   string
	Path   string
	Lang   string
	Output string
}

func getGlob(s string) (glob.Glob, error) {
	if s == "" {
		return nil, nil
	}
	g, err := hglob.GetGlob(s)
	if err != nil {
		return nil, fmt.Errorf("failed to compile Glob %q: %w", s, err)
	}
	return g, nil
}

func compileSegments(f []SegmentMatcherFields) (predicate.P[SegmentMatcherFields], error) {
	if f == nil {
		return func(SegmentMatcherFields) bool { return false }, nil
	}
	var (
		result  predicate.P[SegmentMatcherFields]
		section predicate.P[SegmentMatcherFields]
	)

	addToSection := func(matcherFields SegmentMatcherFields, f func(fields SegmentMatcherFields) string) error {
		s1 := f(matcherFields)
		g, err := getGlob(s1)
		if err != nil {
			return err
		}
		matcher := func(fields SegmentMatcherFields) bool {
			s2 := f(fields)
			if s2 == "" {
				return false
			}
			return g.Match(s2)
		}
		if section == nil {
			section = matcher
		} else {
			section = section.And(matcher)
		}
		return nil
	}

	for _, fields := range f {
		if fields.Kind != "" {
			if err := addToSection(fields, func(fields SegmentMatcherFields) string { return fields.Kind }); err != nil {
				return result, err
			}
		}
		if fields.Path != "" {
			if err := addToSection(fields, func(fields SegmentMatcherFields) string { return fields.Path }); err != nil {
				return result, err
			}
		}
		if fields.Lang != "" {
			if err := addToSection(fields, func(fields SegmentMatcherFields) string { return fields.Lang }); err != nil {
				return result, err
			}
		}
		if fields.Output != "" {
			if err := addToSection(fields, func(fields SegmentMatcherFields) string { return fields.Output }); err != nil {
				return result, err
			}
		}

		if result == nil {
			result = section
		} else {
			result = result.Or(section)
		}
		section = nil

	}

	return result, nil
}

func DecodeSegments(in map[string]any) (*config.ConfigNamespace[map[string]SegmentConfig, Segments], error) {
	buildConfig := func(in any) (Segments, any, error) {
		sms := Segments{
			s: map[string]excludeInclude{},
		}
		m, err := maps.ToStringMapE(in)
		if err != nil {
			return sms, nil, err
		}
		if m == nil {
			m = map[string]any{}
		}
		m = maps.CleanConfigStringMap(m)

		var scfgm map[string]SegmentConfig
		if err := mapstructure.Decode(m, &scfgm); err != nil {
			return sms, nil, err
		}

		for k, v := range scfgm {
			var (
				include predicate.P[SegmentMatcherFields]
				exclude predicate.P[SegmentMatcherFields]
				err     error
			)
			if v.Excludes != nil {
				exclude, err = compileSegments(v.Excludes)
				if err != nil {
					return sms, nil, err
				}
			}
			if v.Includes != nil {
				include, err = compileSegments(v.Includes)
				if err != nil {
					return sms, nil, err
				}
			}

			ei := excludeInclude{
				exclude: exclude,
				include: include,
			}
			sms.s[k] = ei

		}

		return sms, nil, nil
	}

	ns, err := config.DecodeNamespace[map[string]SegmentConfig](in, buildConfig)
	if err != nil {
		return nil, fmt.Errorf("failed to decode segments: %w", err)
	}
	return ns, nil
}
