package analysis

import (
	"fmt"
	"log"
	"net/http"
	"path"
	"sort"
	"strings"

	"strconv"

	"github.com/go-openapi/jsonpointer"
	swspec "github.com/go-openapi/spec"
	"github.com/go-openapi/swag"
)

// FlattenOpts configuration for flattening a swagger specification.
type FlattenOpts struct {
	Spec     *Spec
	BasePath string

	_ struct{} // require keys
}

// ExpandOpts creates a spec.ExpandOptions to configure expanding a specification document.
func (f *FlattenOpts) ExpandOpts(skipSchemas bool) *swspec.ExpandOptions {
	return &swspec.ExpandOptions{RelativeBase: f.BasePath, SkipSchemas: skipSchemas}
}

// Swagger gets the swagger specification for this flatten operation
func (f *FlattenOpts) Swagger() *swspec.Swagger {
	return f.Spec.spec
}

// Flatten an analyzed spec.
//
// To flatten a spec means:
//
// Expand the parameters, responses, path items, parameter items and header items.
// Import external (http, file) references so they become internal to the document.
// Move every inline schema to be a definition with an auto-generated name in a depth-first fashion.
// Rewritten schemas get a vendor extension x-go-gen-location so we know in which package they need to be rendered.
func Flatten(opts FlattenOpts) error {
	// recursively expand responses, parameters, path items and items
	err := swspec.ExpandSpec(opts.Swagger(), opts.ExpandOpts(true))
	if err != nil {
		return err
	}
	opts.Spec.reload() // re-analyze

	// at this point there are no other references left but schemas
	if err := importExternalReferences(&opts); err != nil {
		return err
	}
	opts.Spec.reload() // re-analyze

	// rewrite the inline schemas (schemas that aren't simple types or arrays of simple types)
	if err := nameInlinedSchemas(&opts); err != nil {
		return err
	}
	opts.Spec.reload() // re-analyze

	// TODO: simplifiy known schema patterns to flat objects with properties?
	return nil
}

func nameInlinedSchemas(opts *FlattenOpts) error {
	namer := &inlineSchemaNamer{Spec: opts.Swagger(), Operations: opRefsByRef(gatherOperations(opts.Spec, nil))}
	depthFirst := sortDepthFirst(opts.Spec.allSchemas)

	for _, key := range depthFirst {
		sch := opts.Spec.allSchemas[key]
		if sch.Schema != nil && sch.Schema.Ref.String() == "" && !sch.TopLevel { // inline schema
			asch, err := Schema(SchemaOpts{Schema: sch.Schema, Root: opts.Swagger(), BasePath: opts.BasePath})
			if err != nil {
				return fmt.Errorf("schema analysis [%s]: %v", sch.Ref.String(), err)
			}

			if !asch.IsSimpleSchema { // complex schemas get moved
				if err := namer.Name(key, sch.Schema, asch); err != nil {
					return err
				}
			}
		}
	}
	return nil
}

var depthGroupOrder = []string{"sharedOpParam", "opParam", "codeResponse", "defaultResponse", "definition"}

func sortDepthFirst(data map[string]SchemaRef) (sorted []string) {
	// group by category (shared params, op param, statuscode response, default response, definitions)
	// sort groups internally by number of parts in the key and lexical names
	// flatten groups into a single list of keys
	grouped := make(map[string]keys, len(data))
	for k := range data {
		split := keyParts(k)
		var pk string
		if split.IsSharedOperationParam() {
			pk = "sharedOpParam"
		}
		if split.IsOperationParam() {
			pk = "opParam"
		}
		if split.IsStatusCodeResponse() {
			pk = "codeResponse"
		}
		if split.IsDefaultResponse() {
			pk = "defaultResponse"
		}
		if split.IsDefinition() {
			pk = "definition"
		}
		grouped[pk] = append(grouped[pk], key{len(split), k})
	}

	for _, pk := range depthGroupOrder {
		res := grouped[pk]
		sort.Sort(res)
		for _, v := range res {
			sorted = append(sorted, v.Key)
		}
	}

	return
}

type key struct {
	Segments int
	Key      string
}
type keys []key

func (k keys) Len() int      { return len(k) }
func (k keys) Swap(i, j int) { k[i], k[j] = k[j], k[i] }
func (k keys) Less(i, j int) bool {
	return k[i].Segments > k[j].Segments || (k[i].Segments == k[j].Segments && k[i].Key < k[j].Key)
}

type inlineSchemaNamer struct {
	Spec       *swspec.Swagger
	Operations map[string]opRef
}

func opRefsByRef(oprefs map[string]opRef) map[string]opRef {
	result := make(map[string]opRef, len(oprefs))
	for _, v := range oprefs {
		result[v.Ref.String()] = v
	}
	return result
}

func (isn *inlineSchemaNamer) Name(key string, schema *swspec.Schema, aschema *AnalyzedSchema) error {
	if swspec.Debug {
		log.Printf("naming inlined schema at %s", key)
	}

	parts := keyParts(key)
	for _, name := range namesFromKey(parts, aschema, isn.Operations) {
		if name != "" {
			// create unique name
			newName := uniqifyName(isn.Spec.Definitions, swag.ToJSONName(name))

			// clone schema
			sch, err := cloneSchema(schema)
			if err != nil {
				return err
			}

			// replace values on schema
			if err := rewriteSchemaToRef(isn.Spec, key, swspec.MustCreateRef("#/definitions/"+newName)); err != nil {
				return fmt.Errorf("name inlined schema: %v", err)
			}

			sch.AddExtension("x-go-gen-location", genLocation(parts))
			// fmt.Printf("{\n  %q,\n  \"\",\n  spec.MustCreateRef(%q),\n  \"\",\n},\n", key, "#/definitions/"+newName)
			// save cloned schema to definitions
			saveSchema(isn.Spec, newName, sch)
		}
	}
	return nil
}

func genLocation(parts splitKey) string {
	if parts.IsOperation() {
		return "operations"
	}
	if parts.IsDefinition() {
		return "models"
	}
	return ""
}

func uniqifyName(definitions swspec.Definitions, name string) string {
	if name == "" {
		name = "oaiGen"
	}
	if len(definitions) == 0 {
		return name
	}

	if _, ok := definitions[name]; !ok {
		return name
	}
	name += "OAIGen"
	var idx int
	unique := name
	_, known := definitions[unique]
	for known {
		idx++
		unique = fmt.Sprintf("%s%d", name, idx)
		_, known = definitions[unique]
	}
	return unique
}

func namesFromKey(parts splitKey, aschema *AnalyzedSchema, operations map[string]opRef) []string {
	var baseNames [][]string
	var startIndex int
	if parts.IsOperation() {
		// params
		if parts.IsOperationParam() || parts.IsSharedOperationParam() {
			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"})
					}
				}
			}
		}
		// 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"})
				}
			}
		}
	}

	// definitions
	if parts.IsDefinition() {
		nm := parts.DefinitionName()
		if nm != "" {
			startIndex = 2
			baseNames = append(baseNames, []string{parts.DefinitionName()})
		}
	}

	var result []string
	for _, segments := range baseNames {
		nm := parts.BuildName(segments, startIndex, aschema)
		if nm != "" {
			result = append(result, nm)
		}
	}
	sort.Strings(result)
	return result
}

const (
	pths        = "paths"
	responses   = "responses"
	parameters  = "parameters"
	definitions = "definitions"
)

var ignoredKeys map[string]struct{}

func init() {
	ignoredKeys = map[string]struct{}{
		"schema":     {},
		"properties": {},
		"not":        {},
		"anyOf":      {},
		"oneOf":      {},
	}
}

type splitKey []string

func (s splitKey) IsDefinition() bool {
	return len(s) > 1 && s[0] == definitions
}

func (s splitKey) DefinitionName() string {
	if !s.IsDefinition() {
		return ""
	}
	return s[1]
}

func (s splitKey) BuildName(segments []string, startIndex int, aschema *AnalyzedSchema) string {
	for _, part := range s[startIndex:] {
		if _, ignored := ignoredKeys[part]; !ignored {
			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)
				}
				continue
			}
			segments = append(segments, part)
		}
	}
	return strings.Join(segments, " ")
}

func (s splitKey) IsOperation() bool {
	return len(s) > 1 && s[0] == pths
}

func (s splitKey) IsSharedOperationParam() bool {
	return len(s) > 2 && s[0] == pths && s[2] == parameters
}

func (s splitKey) IsOperationParam() bool {
	return len(s) > 3 && s[0] == pths && s[3] == parameters
}

func (s splitKey) IsOperationResponse() bool {
	return len(s) > 3 && s[0] == pths && s[3] == responses
}

func (s splitKey) IsDefaultResponse() bool {
	return len(s) > 4 && s[0] == pths && s[3] == responses && s[4] == "default"
}

func (s splitKey) IsStatusCodeResponse() bool {
	isInt := func() bool {
		_, err := strconv.Atoi(s[4])
		return err == nil
	}
	return len(s) > 4 && s[0] == pths && s[3] == responses && isInt()
}

func (s splitKey) ResponseName() string {
	if s.IsStatusCodeResponse() {
		code, _ := strconv.Atoi(s[4])
		return http.StatusText(code)
	}
	if s.IsDefaultResponse() {
		return "Default"
	}
	return ""
}

var validMethods map[string]struct{}

func init() {
	validMethods = map[string]struct{}{
		"GET":     {},
		"HEAD":    {},
		"OPTIONS": {},
		"PATCH":   {},
		"POST":    {},
		"PUT":     {},
		"DELETE":  {},
	}
}

func (s splitKey) PathItemRef() swspec.Ref {
	if len(s) < 3 {
		return swspec.Ref{}
	}
	pth, method := s[1], s[2]
	if _, validMethod := validMethods[strings.ToUpper(method)]; !validMethod && !strings.HasPrefix(method, "x-") {
		return swspec.Ref{}
	}
	return swspec.MustCreateRef("#" + path.Join("/", pths, jsonpointer.Escape(pth), strings.ToUpper(method)))
}

func (s splitKey) PathRef() swspec.Ref {
	if !s.IsOperation() {
		return swspec.Ref{}
	}
	return swspec.MustCreateRef("#" + path.Join("/", pths, jsonpointer.Escape(s[1])))
}

func keyParts(key string) splitKey {
	var res []string
	for _, part := range strings.Split(key[1:], "/") {
		if part != "" {
			res = append(res, jsonpointer.Unescape(part))
		}
	}
	return res
}

func rewriteSchemaToRef(spec *swspec.Swagger, key string, ref swspec.Ref) error {
	if swspec.Debug {
		log.Printf("rewriting schema to ref for %s with %s", key, ref.String())
	}
	pth := key[1:]
	ptr, err := jsonpointer.New(pth)
	if err != nil {
		return err
	}

	value, _, err := ptr.Get(spec)
	if err != nil {
		return err
	}

	switch refable := value.(type) {
	case *swspec.Schema:
		return rewriteParentRef(spec, key, ref)
	case *swspec.SchemaOrBool:
		if refable.Schema != nil {
			refable.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
		}
	case *swspec.SchemaOrArray:
		if refable.Schema != nil {
			refable.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
		}
	case swspec.Schema:
		return rewriteParentRef(spec, key, ref)
	default:
		return fmt.Errorf("no schema with ref found at %s for %T", key, value)
	}

	return nil
}

func rewriteParentRef(spec *swspec.Swagger, key string, ref swspec.Ref) error {
	pth := key[1:]
	parent, entry := path.Dir(pth), path.Base(pth)
	if swspec.Debug {
		log.Println("getting schema holder at:", parent)
	}

	pptr, err := jsonpointer.New(parent)
	if err != nil {
		return err
	}
	pvalue, _, err := pptr.Get(spec)
	if err != nil {
		return fmt.Errorf("can't get parent for %s: %v", parent, err)
	}
	if swspec.Debug {
		log.Printf("rewriting holder for %T", pvalue)
	}

	switch container := pvalue.(type) {
	case swspec.Response:
		if err := rewriteParentRef(spec, "#"+parent, ref); err != nil {
			return err
		}

	case *swspec.Response:
		container.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}

	case *swspec.Responses:
		statusCode, err := strconv.Atoi(entry)
		if err != nil {
			return fmt.Errorf("%s not a number: %v", pth, err)
		}
		resp := container.StatusCodeResponses[statusCode]
		resp.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
		container.StatusCodeResponses[statusCode] = resp

	case map[string]swspec.Response:
		resp := container[entry]
		resp.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
		container[entry] = resp

	case swspec.Parameter:
		if err := rewriteParentRef(spec, "#"+parent, ref); err != nil {
			return err
		}

	case map[string]swspec.Parameter:
		param := container[entry]
		param.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
		container[entry] = param

	case []swspec.Parameter:
		idx, err := strconv.Atoi(entry)
		if err != nil {
			return fmt.Errorf("%s not a number: %v", pth, err)
		}
		param := container[idx]
		param.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
		container[idx] = param

	case swspec.Definitions:
		container[entry] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}

	case map[string]swspec.Schema:
		container[entry] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}

	case []swspec.Schema:
		idx, err := strconv.Atoi(entry)
		if err != nil {
			return fmt.Errorf("%s not a number: %v", pth, err)
		}
		container[idx] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}

	case *swspec.SchemaOrArray:
		idx, err := strconv.Atoi(entry)
		if err != nil {
			return fmt.Errorf("%s not a number: %v", pth, err)
		}
		container.Schemas[idx] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
	default:
		return fmt.Errorf("unhandled parent schema rewrite %s (%T)", key, pvalue)
	}
	return nil
}

func cloneSchema(schema *swspec.Schema) (*swspec.Schema, error) {
	var sch swspec.Schema
	if err := swag.FromDynamicJSON(schema, &sch); err != nil {
		return nil, fmt.Errorf("name inlined schema: %v", err)
	}
	return &sch, nil
}

func importExternalReferences(opts *FlattenOpts) error {
	groupedRefs := reverseIndexForSchemaRefs(opts)

	for refStr, entry := range groupedRefs {
		if !entry.Ref.HasFragmentOnly {
			if swspec.Debug {
				log.Printf("importing external schema for [%s] from %s", strings.Join(entry.Keys, ", "), refStr)
			}
			// resolve to actual schema
			sch, err := swspec.ResolveRefWithBase(opts.Swagger(), &entry.Ref, opts.ExpandOpts(false))
			if err != nil {
				return err
			}
			if sch == nil {
				return fmt.Errorf("no schema found at %s for [%s]", refStr, strings.Join(entry.Keys, ", "))
			}
			if swspec.Debug {
				log.Printf("importing external schema for [%s] from %s", strings.Join(entry.Keys, ", "), refStr)
			}

			// generate a unique name
			newName := uniqifyName(opts.Swagger().Definitions, nameFromRef(entry.Ref))
			if swspec.Debug {
				log.Printf("new name for [%s]: %s", strings.Join(entry.Keys, ", "), newName)
			}

			// rewrite the external refs to local ones
			for _, key := range entry.Keys {
				if err := updateRef(opts.Swagger(), key, swspec.MustCreateRef("#"+path.Join("/definitions", newName))); err != nil {
					return err
				}
			}

			// add the resolved schema to the definitions
			saveSchema(opts.Swagger(), newName, sch)
		}
	}
	return nil
}

type refRevIdx struct {
	Ref  swspec.Ref
	Keys []string
}

func reverseIndexForSchemaRefs(opts *FlattenOpts) map[string]refRevIdx {
	collected := make(map[string]refRevIdx)
	for key, schRef := range opts.Spec.references.schemas {
		if entry, ok := collected[schRef.String()]; ok {
			entry.Keys = append(entry.Keys, key)
			collected[schRef.String()] = entry
		} else {
			collected[schRef.String()] = refRevIdx{
				Ref:  schRef,
				Keys: []string{key},
			}
		}
	}
	return collected
}

func nameFromRef(ref swspec.Ref) string {
	u := ref.GetURL()
	if u.Fragment != "" {
		return swag.ToJSONName(path.Base(u.Fragment))
	}
	if u.Path != "" {
		bn := path.Base(u.Path)
		if bn != "" && bn != "/" {
			ext := path.Ext(bn)
			if ext != "" {
				return swag.ToJSONName(bn[:len(bn)-len(ext)])
			}
			return swag.ToJSONName(bn)
		}
	}
	return swag.ToJSONName(strings.Replace(u.Host, ".", " ", -1))
}

func saveSchema(spec *swspec.Swagger, name string, schema *swspec.Schema) {
	if schema == nil {
		return
	}
	if spec.Definitions == nil {
		spec.Definitions = make(map[string]swspec.Schema, 150)
	}
	spec.Definitions[name] = *schema
}

func updateRef(spec *swspec.Swagger, key string, ref swspec.Ref) error {
	if swspec.Debug {
		log.Printf("updating ref for %s with %s", key, ref.String())
	}
	pth := key[1:]
	ptr, err := jsonpointer.New(pth)
	if err != nil {
		return err
	}

	value, _, err := ptr.Get(spec)
	if err != nil {
		return err
	}

	switch refable := value.(type) {
	case *swspec.Schema:
		refable.Ref = ref
	case *swspec.SchemaOrBool:
		if refable.Schema != nil {
			refable.Schema.Ref = ref
		}
	case *swspec.SchemaOrArray:
		if refable.Schema != nil {
			refable.Schema.Ref = ref
		}
	case swspec.Schema:
		parent, entry := path.Dir(pth), path.Base(pth)
		if swspec.Debug {
			log.Println("getting schema holder at:", parent)
		}

		pptr, err := jsonpointer.New(parent)
		if err != nil {
			return err
		}
		pvalue, _, err := pptr.Get(spec)
		if err != nil {
			return fmt.Errorf("can't get parent for %s: %v", parent, err)
		}

		switch container := pvalue.(type) {
		case swspec.Definitions:
			container[entry] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}

		case map[string]swspec.Schema:
			container[entry] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}

		case []swspec.Schema:
			idx, err := strconv.Atoi(entry)
			if err != nil {
				return fmt.Errorf("%s not a number: %v", pth, err)
			}
			container[idx] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}

		case *swspec.SchemaOrArray:
			idx, err := strconv.Atoi(entry)
			if err != nil {
				return fmt.Errorf("%s not a number: %v", pth, err)
			}
			container.Schemas[idx] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}

		}

	default:
		return fmt.Errorf("no schema with ref found at %s for %T", key, value)
	}

	return nil
}

func containsString(names []string, name string) bool {
	for _, nm := range names {
		if nm == name {
			return true
		}
	}
	return false
}

type opRef struct {
	Method string
	Path   string
	Key    string
	ID     string
	Op     *swspec.Operation
	Ref    swspec.Ref
}

type opRefs []opRef

func (o opRefs) Len() int           { return len(o) }
func (o opRefs) Swap(i, j int)      { o[i], o[j] = o[j], o[i] }
func (o opRefs) Less(i, j int) bool { return o[i].Key < o[j].Key }

func gatherOperations(specDoc *Spec, operationIDs []string) map[string]opRef {
	var oprefs opRefs

	for method, pathItem := range specDoc.Operations() {
		for pth, operation := range pathItem {
			vv := *operation
			oprefs = append(oprefs, opRef{
				Key:    swag.ToGoName(strings.ToLower(method) + " " + pth),
				Method: method,
				Path:   pth,
				ID:     vv.ID,
				Op:     &vv,
				Ref:    swspec.MustCreateRef("#" + path.Join("/paths", jsonpointer.Escape(pth), method)),
			})
		}
	}

	sort.Sort(oprefs)

	operations := make(map[string]opRef)
	for _, opr := range oprefs {
		nm := opr.ID
		if nm == "" {
			nm = opr.Key
		}

		oo, found := operations[nm]
		if found && oo.Method != opr.Method && oo.Path != opr.Path {
			nm = opr.Key
		}
		if len(operationIDs) == 0 || containsString(operationIDs, opr.ID) || containsString(operationIDs, nm) {
			opr.ID = nm
			opr.Op.ID = nm
			operations[nm] = opr
		}
	}

	return operations
}
