package analysis

import (
	"fmt"
	"path"
	"sort"
	"strings"

	"github.com/go-openapi/analysis/internal/flatten/operations"
	"github.com/go-openapi/analysis/internal/flatten/replace"
	"github.com/go-openapi/analysis/internal/flatten/schutils"
	"github.com/go-openapi/analysis/internal/flatten/sortref"
	"github.com/go-openapi/spec"
	"github.com/go-openapi/swag/mangling"
)

// InlineSchemaNamer finds a new name for an inlined type
type InlineSchemaNamer struct {
	Spec           *spec.Swagger
	Operations     map[string]operations.OpRef
	flattenContext *context
	opts           *FlattenOpts
}

// Name yields a new name for the inline schema
func (isn *InlineSchemaNamer) Name(key string, schema *spec.Schema, aschema *AnalyzedSchema) error {
	debugLog("naming inlined schema at %s", key)

	parts := sortref.KeyParts(key)
	for _, name := range namesFromKey(parts, aschema, isn.Operations) {
		if name == "" {
			continue
		}

		// create unique name
		mangle := mangler(isn.opts)
		newName, isOAIGen := uniqifyName(isn.Spec.Definitions, mangle(name))

		// clone schema
		sch := schutils.Clone(schema)

		// replace values on schema
		debugLog("rewriting schema to ref: key=%s with new name: %s", key, newName)
		if err := replace.RewriteSchemaToRef(isn.Spec, key,
			spec.MustCreateRef(path.Join(definitionsPath, newName))); err != nil {
			return ErrInlineDefinition(newName, err)
		}

		// rewrite any dependent $ref pointing to this place,
		// when not already pointing to a top-level definition.
		//
		// NOTE: this is important if such referers use arbitrary JSON pointers.
		an := New(isn.Spec)
		for k, v := range an.references.allRefs {
			r, erd := replace.DeepestRef(isn.opts.Swagger(), isn.opts.ExpandOpts(false), v)
			if erd != nil {
				return ErrAtKey(k, erd)
			}

			if isn.opts.flattenContext != nil {
				isn.opts.flattenContext.warnings = append(isn.opts.flattenContext.warnings, r.Warnings...)
			}

			if r.Ref.String() != key && (r.Ref.String() != path.Join(definitionsPath, newName) || path.Dir(v.String()) == definitionsPath) {
				continue
			}

			debugLog("found a $ref to a rewritten schema: %s points to %s", k, v.String())

			// rewrite $ref to the new target
			if err := replace.UpdateRef(isn.Spec, k,
				spec.MustCreateRef(path.Join(definitionsPath, newName))); err != nil {
				return err
			}
		}

		// NOTE: this extension is currently not used by go-swagger (provided for information only)
		sch.AddExtension("x-go-gen-location", GenLocation(parts))

		// save cloned schema to definitions
		schutils.Save(isn.Spec, newName, sch)

		// keep track of created refs
		if isn.flattenContext == nil {
			continue
		}

		debugLog("track created ref: key=%s, newName=%s, isOAIGen=%t", key, newName, isOAIGen)
		resolved := false

		if _, ok := isn.flattenContext.newRefs[key]; ok {
			resolved = isn.flattenContext.newRefs[key].resolved
		}

		isn.flattenContext.newRefs[key] = &newRef{
			key:      key,
			newName:  newName,
			path:     path.Join(definitionsPath, newName),
			isOAIGen: isOAIGen,
			resolved: resolved,
			schema:   sch,
		}
	}

	return nil
}

// uniqifyName yields a unique name for a definition
func uniqifyName(definitions spec.Definitions, name string) (string, bool) {
	isOAIGen := false
	if name == "" {
		name = "oaiGen"
		isOAIGen = true
	}

	if len(definitions) == 0 {
		return name, isOAIGen
	}

	unq := true
	for k := range definitions {
		if strings.EqualFold(k, name) {
			unq = false

			break
		}
	}

	if unq {
		return name, isOAIGen
	}

	name += "OAIGen"
	isOAIGen = true
	var idx int
	unique := name
	_, known := definitions[unique]

	for known {
		idx++
		unique = fmt.Sprintf("%s%d", name, idx)
		_, known = definitions[unique]
	}

	return unique, isOAIGen
}

func namesFromKey(parts sortref.SplitKey, aschema *AnalyzedSchema, operations map[string]operations.OpRef) []string {
	var (
		baseNames  [][]string
		startIndex int
	)

	switch {
	case parts.IsOperation():
		baseNames, startIndex = namesForOperation(parts, operations)
	case parts.IsDefinition():
		baseNames, startIndex = namesForDefinition(parts)
	default:
		// this a non-standard pointer: build a name by concatenating its parts
		baseNames = [][]string{parts}
		startIndex = len(baseNames) + 1
	}

	result := make([]string, 0, len(baseNames))
	for _, segments := range baseNames {
		nm := parts.BuildName(segments, startIndex, partAdder(aschema))
		if nm == "" {
			continue
		}

		result = append(result, nm)
	}
	sort.Strings(result)

	debugLog("names from parts: %v => %v", parts, result)
	return result
}

func namesForParam(parts sortref.SplitKey, operations map[string]operations.OpRef) ([][]string, int) {
	var (
		baseNames  [][]string
		startIndex int
	)

	piref := parts.PathItemRef()
	if piref.String() != "" && parts.IsOperationParam() {
		if op, ok := operations[piref.String()]; ok {
			startIndex = 5
			baseNames = append(baseNames, []string{op.ID, "params", "body"})
		}
	} else if parts.IsSharedOperationParam() {
		pref := parts.PathRef()
		for k, v := range operations {
			if strings.HasPrefix(k, pref.String()) {
				startIndex = 4
				baseNames = append(baseNames, []string{v.ID, "params", "body"})
			}
		}
	}

	return baseNames, startIndex
}

func namesForOperation(parts sortref.SplitKey, operations map[string]operations.OpRef) ([][]string, int) {
	var (
		baseNames  [][]string
		startIndex int
	)

	// params
	if parts.IsOperationParam() || parts.IsSharedOperationParam() {
		baseNames, startIndex = namesForParam(parts, operations)
	}

	// responses
	if parts.IsOperationResponse() {
		piref := parts.PathItemRef()
		if piref.String() != "" {
			if op, ok := operations[piref.String()]; ok {
				startIndex = 6
				baseNames = append(baseNames, []string{op.ID, parts.ResponseName(), "body"})
			}
		}
	}

	return baseNames, startIndex
}

const (
	minStartIndex = 2
	minSegments   = 2
)

func namesForDefinition(parts sortref.SplitKey) ([][]string, int) {
	nm := parts.DefinitionName()
	if nm != "" {
		return [][]string{{parts.DefinitionName()}}, minStartIndex
	}

	return [][]string{}, 0
}

// partAdder knows how to interpret a schema when it comes to build a name from parts
func partAdder(aschema *AnalyzedSchema) sortref.PartAdder {
	return func(part string) []string {
		segments := make([]string, 0, minSegments)

		if part == "items" || part == "additionalItems" {
			if aschema.IsTuple || aschema.IsTupleWithExtra {
				segments = append(segments, "tuple")
			} else {
				segments = append(segments, "items")
			}

			if part == "additionalItems" {
				segments = append(segments, part)
			}

			return segments
		}

		segments = append(segments, part)

		return segments
	}
}

func mangler(o *FlattenOpts) func(string) string {
	if o.KeepNames {
		return func(in string) string { return in }
	}
	mangler := mangling.NewNameMangler()

	return mangler.ToJSONName
}

func nameFromRef(ref spec.Ref, o *FlattenOpts) string {
	mangle := mangler(o)

	u := ref.GetURL()
	if u.Fragment != "" {
		return mangle(path.Base(u.Fragment))
	}

	if u.Path != "" {
		bn := path.Base(u.Path)
		if bn != "" && bn != "/" {
			ext := path.Ext(bn)
			if ext != "" {
				return mangle(bn[:len(bn)-len(ext)])
			}

			return mangle(bn)
		}
	}

	return mangle(strings.ReplaceAll(u.Host, ".", " "))
}

// GenLocation indicates from which section of the specification (models or operations) a definition has been created.
//
// This is reflected in the output spec with a "x-go-gen-location" extension. At the moment, this is provided
// for information only.
func GenLocation(parts sortref.SplitKey) string {
	switch {
	case parts.IsOperation():
		return "operations"
	case parts.IsDefinition():
		return "models"
	default:
		return ""
	}
}
