package ls

import (
	"slices"

	"github.com/microsoft/typescript-go/internal/ast"
	"github.com/microsoft/typescript-go/internal/astnav"
	"github.com/microsoft/typescript-go/internal/core"
	"github.com/microsoft/typescript-go/internal/debug"
	"github.com/microsoft/typescript-go/internal/ls/change"
	"github.com/microsoft/typescript-go/internal/ls/lsutil"
	"github.com/microsoft/typescript-go/internal/ls/organizeimports"
	"github.com/microsoft/typescript-go/internal/stringutil"
)

type Import struct {
	name          string
	kind          ImportKind // ImportKindCommonJS | ImportKindNamespace
	addAsTypeOnly AddAsTypeOnly
	propertyName  string // Use when needing to generate an `ImportSpecifier with a `propertyName`; the name preceding "as" keyword (propertyName = "" when "as" is absent)
}

func addNamespaceQualifier(ct *change.Tracker, sourceFile *ast.SourceFile, qualification *Qualification) {
	ct.InsertText(sourceFile, qualification.usagePosition, qualification.namespacePrefix+".")
}

func (ls *LanguageService) doAddExistingFix(
	ct *change.Tracker,
	sourceFile *ast.SourceFile,
	clause *ast.Node, // ImportClause | ObjectBindingPattern,
	defaultImport *Import,
	namedImports []*Import,
	// removeExistingImportSpecifiers *core.Set[ImportSpecifier | BindingElement] // !!! remove imports not implemented
) {
	switch clause.Kind {
	case ast.KindObjectBindingPattern:
		if clause.Kind == ast.KindObjectBindingPattern {
			// bindingPattern := clause.AsBindingPattern()
			// !!! adding *and* removing imports not implemented
			// if (removeExistingImportSpecifiers && core.Some(bindingPattern.Elements, func(e *ast.Node) bool {
			//     return removeExistingImportSpecifiers.Has(e)
			// })) {
			// If we're both adding and removing elements, just replace and reprint the whole
			// node. The change tracker doesn't understand all the operations and can insert or
			// leave behind stray commas.
			// ct.replaceNode(
			//     sourceFile,
			//     bindingPattern,
			// ct.NodeFactory.NewObjectBindingPattern([
			//     ...bindingPattern.Elements.Filter(func(e *ast.Node) bool {
			//         return !removeExistingImportSpecifiers.Has(e)
			//     }),
			//     ...defaultImport ? [ct.NodeFactory.createBindingElement(/*dotDotDotToken*/ nil, /*propertyName*/ "default", defaultImport.name)] : emptyArray,
			//     ...namedImports.map(i => ct.NodeFactory.createBindingElement(/*dotDotDotToken*/ nil, i.propertyName, i.name)),
			// ]),
			// )
			//     return
			// }
			if defaultImport != nil {
				addElementToBindingPattern(ct, sourceFile, clause, defaultImport.name, ptrTo("default"))
			}
			for _, specifier := range namedImports {
				addElementToBindingPattern(ct, sourceFile, clause, specifier.name, &specifier.propertyName)
			}
			return
		}
	case ast.KindImportClause:

		importClause := clause.AsImportClause()

		// promoteFromTypeOnly = true if we need to promote the entire original clause from type only
		promoteFromTypeOnly := importClause.IsTypeOnly() && core.Some(append(namedImports, defaultImport), func(i *Import) bool {
			if i == nil {
				return false
			}
			return i.addAsTypeOnly == AddAsTypeOnlyNotAllowed
		})

		existingSpecifiers := []*ast.Node{} // []*ast.ImportSpecifier
		if importClause.NamedBindings != nil && importClause.NamedBindings.Kind == ast.KindNamedImports {
			existingSpecifiers = importClause.NamedBindings.Elements()
		}

		if defaultImport != nil {
			debug.Assert(clause.Name() == nil, "Cannot add a default import to an import clause that already has one")
			ct.InsertNodeAt(sourceFile, core.TextPos(astnav.GetStartOfNode(clause, sourceFile, false)), ct.NodeFactory.NewIdentifier(defaultImport.name), change.NodeOptions{Suffix: ", "})
		}

		if len(namedImports) > 0 {
			specifierComparer, isSorted := organizeimports.GetNamedImportSpecifierComparerWithDetection(importClause.Parent, sourceFile, ls.UserPreferences())
			newSpecifiers := core.Map(namedImports, func(namedImport *Import) *ast.Node {
				var identifier *ast.Node
				if namedImport.propertyName != "" {
					identifier = ct.NodeFactory.NewIdentifier(namedImport.propertyName).AsIdentifier().AsNode()
				}
				return ct.NodeFactory.NewImportSpecifier(
					(!importClause.IsTypeOnly() || promoteFromTypeOnly) && shouldUseTypeOnly(namedImport.addAsTypeOnly, ls.UserPreferences()),
					identifier,
					ct.NodeFactory.NewIdentifier(namedImport.name),
				)
			})
			slices.SortFunc(newSpecifiers, specifierComparer)

			// !!! remove imports not implemented
			// if (removeExistingImportSpecifiers) {
			//     // If we're both adding and removing specifiers, just replace and reprint the whole
			//     // node. The change tracker doesn't understand all the operations and can insert or
			//     // leave behind stray commas.
			//     ct.replaceNode(
			//         sourceFile,
			//         importClause.NamedBindings,
			//         ct.NodeFactory.updateNamedImports(
			//             importClause.NamedBindings.AsNamedImports(),
			//             append(core.Filter(existingSpecifiers, func (s *ast.ImportSpecifier) bool {return !removeExistingImportSpecifiers.Has(s)}), newSpecifiers...), // !!! sort with specifierComparer
			//         ),
			//     );
			//
			if len(existingSpecifiers) > 0 && isSorted != core.TSFalse {
				// The sorting preference computed earlier may or may not have validated that these particular
				// import specifiers are sorted. If they aren't, `getImportSpecifierInsertionIndex` will return
				// nonsense. So if there are existing specifiers, even if we know the sorting preference, we
				// need to ensure that the existing specifiers are sorted according to the preference in order
				// to do a sorted insertion.

				// If we're promoting the clause from type-only, we need to transform the existing imports
				// before attempting to insert the new named imports (for comparison purposes only)
				specsToCompareAgainst := existingSpecifiers
				if promoteFromTypeOnly && len(existingSpecifiers) > 0 {
					specsToCompareAgainst = core.Map(existingSpecifiers, func(e *ast.Node) *ast.Node {
						spec := e.AsImportSpecifier()
						var propertyName *ast.Node
						if spec.PropertyName != nil {
							propertyName = spec.PropertyName
						}
						syntheticSpec := ct.NodeFactory.NewImportSpecifier(
							true, // isTypeOnly
							propertyName,
							spec.Name(),
						)
						return syntheticSpec
					})
				}

				for _, spec := range newSpecifiers {
					insertionIndex := organizeimports.GetImportSpecifierInsertionIndex(specsToCompareAgainst, spec, specifierComparer)
					ct.InsertImportSpecifierAtIndex(sourceFile, spec, importClause.NamedBindings, insertionIndex)
				}
			} else if len(existingSpecifiers) > 0 && isSorted.IsTrue() {
				// Existing specifiers are sorted, so insert each new specifier at the correct position
				for _, spec := range newSpecifiers {
					insertionIndex := organizeimports.GetImportSpecifierInsertionIndex(existingSpecifiers, spec, specifierComparer)
					if insertionIndex >= len(existingSpecifiers) {
						// Insert at the end
						ct.InsertNodeInListAfter(sourceFile, existingSpecifiers[len(existingSpecifiers)-1], spec.AsNode(), existingSpecifiers)
					} else {
						// Insert before the element at insertionIndex
						ct.InsertNodeInListAfter(sourceFile, existingSpecifiers[insertionIndex], spec.AsNode(), existingSpecifiers)
					}
				}
			} else if len(existingSpecifiers) > 0 {
				// Existing specifiers may not be sorted, append to the end
				for _, spec := range newSpecifiers {
					ct.InsertNodeInListAfter(sourceFile, existingSpecifiers[len(existingSpecifiers)-1], spec.AsNode(), existingSpecifiers)
				}
			} else {
				if len(newSpecifiers) > 0 {
					namedImports := ct.NodeFactory.NewNamedImports(ct.NodeFactory.NewNodeList(newSpecifiers))
					if importClause.NamedBindings != nil {
						ct.ReplaceNode(sourceFile, importClause.NamedBindings, namedImports, nil)
					} else {
						if clause.Name() == nil {
							panic("Import clause must have either named imports or a default import")
						}
						ct.InsertNodeAfter(sourceFile, clause.Name(), namedImports)
					}
				}
			}
		}

		if promoteFromTypeOnly {
			// Delete the 'type' keyword from the import clause
			typeKeyword := getTypeKeywordOfTypeOnlyImport(importClause, sourceFile)
			ct.Delete(sourceFile, typeKeyword)

			// Add 'type' modifier to existing specifiers (not newly added ones)
			// We preserve the type-onlyness of existing specifiers regardless of whether
			// it would make a difference in emit (user preference).
			if len(existingSpecifiers) > 0 {
				for _, specifier := range existingSpecifiers {
					if !specifier.AsImportSpecifier().IsTypeOnly {
						ct.InsertModifierBefore(sourceFile, ast.KindTypeKeyword, specifier)
					}
				}
			}
		}
	default:
		panic("Unsupported clause kind: " + clause.Kind.String() + "for doAddExistingFix")
	}
}

func getTypeKeywordOfTypeOnlyImport(importClause *ast.ImportClause, sourceFile *ast.SourceFile) *ast.Node {
	debug.Assert(importClause.IsTypeOnly(), "import clause must be type-only")
	// The first child of a type-only import clause is the 'type' keyword
	// import type { foo } from './bar'
	//        ^^^^
	typeKeyword := astnav.FindChildOfKind(importClause.AsNode(), ast.KindTypeKeyword, sourceFile)
	debug.Assert(typeKeyword != nil, "type-only import clause should have a type keyword")
	return typeKeyword
}

func addElementToBindingPattern(ct *change.Tracker, sourceFile *ast.SourceFile, bindingPattern *ast.Node, name string, propertyName *string) {
	element := newBindingElementFromNameAndPropertyName(ct, name, propertyName)
	if len(bindingPattern.Elements()) > 0 {
		ct.InsertNodeInListAfter(sourceFile, bindingPattern.Elements()[len(bindingPattern.Elements())-1], element, nil)
	} else {
		ct.ReplaceNode(sourceFile, bindingPattern, ct.NodeFactory.NewBindingPattern(
			ast.KindObjectBindingPattern,
			ct.NodeFactory.NewNodeList([]*ast.Node{element}),
		), nil)
	}
}

func newBindingElementFromNameAndPropertyName(ct *change.Tracker, name string, propertyName *string) *ast.Node {
	var newPropertyNameIdentifier *ast.Node
	if propertyName != nil {
		newPropertyNameIdentifier = ct.NodeFactory.NewIdentifier(*propertyName)
	}
	return ct.NodeFactory.NewBindingElement(
		nil, /*dotDotDotToken*/
		newPropertyNameIdentifier,
		ct.NodeFactory.NewIdentifier(name),
		nil, /* initializer */
	)
}

func (ls *LanguageService) insertImports(ct *change.Tracker, sourceFile *ast.SourceFile, imports []*ast.Statement, blankLineBetween bool) {
	var existingImportStatements []*ast.Statement

	if imports[0].Kind == ast.KindVariableStatement {
		existingImportStatements = core.Filter(sourceFile.Statements.Nodes, ast.IsRequireVariableStatement)
	} else {
		existingImportStatements = core.Filter(sourceFile.Statements.Nodes, ast.IsAnyImportSyntax)
	}
	comparer, isSorted := organizeimports.GetOrganizeImportsStringComparerWithDetection(existingImportStatements, ls.UserPreferences())
	sortedNewImports := slices.Clone(imports)
	slices.SortFunc(sortedNewImports, func(a, b *ast.Statement) int {
		return organizeimports.CompareImportsOrRequireStatements(a, b, comparer)
	})
	// !!! FutureSourceFile
	// if !isFullSourceFile(sourceFile) {
	//     for _, newImport := range sortedNewImports {
	//         // Insert one at a time to send correct original source file for accurate text reuse
	//         // when some imports are cloned from existing ones in other files.
	//         ct.insertStatementsInNewFile(sourceFile.fileName, []*ast.Node{newImport}, ast.GetSourceFileOfNode(getOriginalNode(newImport)))
	//     }
	// return;
	// }

	if len(existingImportStatements) > 0 && isSorted {
		// Existing imports are sorted, insert each new import at the correct position
		for _, newImport := range sortedNewImports {
			insertionIndex := organizeimports.GetImportDeclarationInsertIndex(existingImportStatements, newImport, func(a, b *ast.Statement) stringutil.Comparison {
				return organizeimports.CompareImportsOrRequireStatements(a, b, comparer)
			})
			if insertionIndex == 0 {
				// If the first import is top-of-file, insert after the leading comment which is likely the header
				ct.InsertNodeAt(sourceFile, core.TextPos(astnav.GetStartOfNode(existingImportStatements[0], sourceFile, false)), newImport.AsNode(), change.NodeOptions{})
			} else {
				prevImport := existingImportStatements[insertionIndex-1]
				ct.InsertNodeAfter(sourceFile, prevImport.AsNode(), newImport.AsNode())
			}
		}
	} else if len(existingImportStatements) > 0 {
		ct.InsertNodesAfter(sourceFile, existingImportStatements[len(existingImportStatements)-1], sortedNewImports)
	} else {
		ct.InsertAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween)
	}
}

func makeImport(ct *change.Tracker, defaultImport *ast.IdentifierNode, namedImports []*ast.Node, moduleSpecifier *ast.Expression, isTypeOnly bool) *ast.Statement {
	var newNamedImports *ast.Node
	if len(namedImports) > 0 {
		newNamedImports = ct.NodeFactory.NewNamedImports(ct.NodeFactory.NewNodeList(namedImports))
	}
	var importClause *ast.Node
	if defaultImport != nil || newNamedImports != nil {
		importClause = ct.NodeFactory.NewImportClause(core.IfElse(isTypeOnly, ast.KindTypeKeyword, ast.KindUnknown), defaultImport, newNamedImports)
	}
	return ct.NodeFactory.NewImportDeclaration( /*modifiers*/ nil, importClause, moduleSpecifier, nil /*attributes*/)
}

func (ls *LanguageService) getNewImports(
	ct *change.Tracker,
	moduleSpecifier string,
	quotePreference quotePreference,
	defaultImport *Import,
	namedImports []*Import,
	namespaceLikeImport *Import, // { importKind: ImportKind.CommonJS | ImportKind.Namespace; }
	compilerOptions *core.CompilerOptions,
) []*ast.Statement {
	moduleSpecifierStringLiteral := ct.NodeFactory.NewStringLiteral(moduleSpecifier)
	if quotePreference == quotePreferenceSingle {
		moduleSpecifierStringLiteral.AsStringLiteral().TokenFlags |= ast.TokenFlagsSingleQuote
	}
	var statements []*ast.Statement // []AnyImportSyntax
	if defaultImport != nil || len(namedImports) > 0 {
		// `verbatimModuleSyntax` should prefer top-level `import type` -
		// even though it's not an error, it would add unnecessary runtime emit.
		topLevelTypeOnly := (defaultImport == nil || needsTypeOnly(defaultImport.addAsTypeOnly)) &&
			core.Every(namedImports, func(i *Import) bool { return needsTypeOnly(i.addAsTypeOnly) }) ||
			(compilerOptions.VerbatimModuleSyntax.IsTrue() || ls.UserPreferences().PreferTypeOnlyAutoImports) &&
				(defaultImport == nil || defaultImport.addAsTypeOnly != AddAsTypeOnlyNotAllowed) &&
				!core.Some(namedImports, func(i *Import) bool { return i.addAsTypeOnly == AddAsTypeOnlyNotAllowed })

		var defaultImportNode *ast.Node
		if defaultImport != nil {
			defaultImportNode = ct.NodeFactory.NewIdentifier(defaultImport.name)
		}

		statements = append(statements, makeImport(ct, defaultImportNode, core.Map(namedImports, func(namedImport *Import) *ast.Node {
			var namedImportPropertyName *ast.Node
			if namedImport.propertyName != "" {
				namedImportPropertyName = ct.NodeFactory.NewIdentifier(namedImport.propertyName)
			}
			return ct.NodeFactory.NewImportSpecifier(
				!topLevelTypeOnly && shouldUseTypeOnly(namedImport.addAsTypeOnly, ls.UserPreferences()),
				namedImportPropertyName,
				ct.NodeFactory.NewIdentifier(namedImport.name),
			)
		}), moduleSpecifierStringLiteral, topLevelTypeOnly))
	}

	if namespaceLikeImport != nil {
		var declaration *ast.Statement
		if namespaceLikeImport.kind == ImportKindCommonJS {
			declaration = ct.NodeFactory.NewImportEqualsDeclaration(
				/*modifiers*/ nil,
				shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, ls.UserPreferences()),
				ct.NodeFactory.NewIdentifier(namespaceLikeImport.name),
				ct.NodeFactory.NewExternalModuleReference(moduleSpecifierStringLiteral),
			)
		} else {
			declaration = ct.NodeFactory.NewImportDeclaration(
				/*modifiers*/ nil,
				ct.NodeFactory.NewImportClause(
					/*phaseModifier*/ core.IfElse(shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, ls.UserPreferences()), ast.KindTypeKeyword, ast.KindUnknown),
					/*name*/ nil,
					ct.NodeFactory.NewNamespaceImport(ct.NodeFactory.NewIdentifier(namespaceLikeImport.name)),
				),
				moduleSpecifierStringLiteral,
				/*attributes*/ nil,
			)
		}
		statements = append(statements, declaration)
	}
	if len(statements) == 0 {
		panic("No statements to insert for new imports")
	}
	return statements
}

func needsTypeOnly(addAsTypeOnly AddAsTypeOnly) bool {
	return addAsTypeOnly == AddAsTypeOnlyRequired
}

func shouldUseTypeOnly(addAsTypeOnly AddAsTypeOnly, preferences *lsutil.UserPreferences) bool {
	return needsTypeOnly(addAsTypeOnly) || addAsTypeOnly != AddAsTypeOnlyNotAllowed && preferences.PreferTypeOnlyAutoImports
}
