//
// Copyright 2021 Sergey Penkovsky (sergey.penkovsky@gmail.com)
// 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
//      https://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.
//

import 'package:analyzer/dart/element/element2.dart';

import 'bind_parameters_spec.dart';
import 'metadata_utils.dart';
import 'exceptions.dart';
import 'type_parser.dart';
import 'annotation_validator.dart';

/// Enum representing the binding annotation applied to a module method.
enum BindingType {
  /// Direct instance returned from the method (@instance).
  instance,

  /// Provider/factory function (@provide).
  provide,
}

/// ---------------------------------------------------------------------------
/// BindSpec
///
/// Describes a DI container binding as generated from a single public factory,
/// instance, or provider method of a module (annotated with @instance or @provide).
///
/// Includes all annotation-driven parameters required to generate valid DI
/// registration Dart code in CherryPick:
/// - Return type
/// - Provider method name
/// - Singleton flag
/// - Named identifier (from @named)
/// - List of resolved or runtime (@params) parameters
/// - Binding mode (instance/provide)
/// - Async and parametric variants
///
/// ## Example usage
/// ```dart
/// // Suppose @provide() Api api(@named('test') Client client)
/// final bindSpec = BindSpec.fromMethod(methodElement);
/// print(bindSpec.generateBind(2)); //   bind<Api>().toProvide(() => api(currentScope.resolve<Client>(named: 'test')));
/// ```
/// ---------------------------------------------------------------------------
class BindSpec {
  /// The type this binding provides (e.g. SomeService)
  final String returnType;

  /// Binding provider/factory method name
  final String methodName;

  /// Named identifier for DI resolution (null if unnamed)
  final String? named;

  /// If true, binding is registered as singleton in DI
  final bool isSingleton;

  /// Provider/factory method parameters (in order)
  final List<BindParameterSpec> parameters;

  /// Instance vs provider mode, from annotation choice
  final BindingType bindingType;

  /// Async flag for .toInstanceAsync()
  final bool isAsyncInstance;

  /// Async flag for .toProvideAsync()
  final bool isAsyncProvide;

  /// True if a @params runtime parameter is present
  final bool hasParams;

  BindSpec({
    required this.returnType,
    required this.methodName,
    required this.isSingleton,
    required this.parameters,
    this.named,
    required this.bindingType,
    required this.isAsyncInstance,
    required this.isAsyncProvide,
    required this.hasParams,
  });

  /// Generates a Dart code line for binding registration.
  ///
  /// Example (single-line):
  ///   bind<Api>().toProvide(() => provideApi(currentScope.resolve<Client>(named: 'test'))).withName('prod').singleton();
  ///
  /// The [indent] argument sets the space indentation for pretty-printing.
  String generateBind(int indent) {
    final indentStr = ' ' * indent;
    final provide = _generateProvideClause(indent);
    final postfix = _generatePostfix();

    // Create the full single-line version first
    final singleLine = '${indentStr}bind<$returnType>()$provide$postfix;';

    // Check if we need multiline formatting
    final needsMultiline = singleLine.length > 80 || provide.contains('\n');

    if (!needsMultiline) {
      return singleLine;
    }

    // For multiline formatting, check if we need to break after bind<Type>()
    if (provide.contains('\n')) {
      // Provider clause is already multiline
      if (postfix.isNotEmpty) {
        // If there's a postfix, break after bind<Type>()
        final multilinePostfix = _generateMultilinePostfix(indent);
        return '${indentStr}bind<$returnType>()'
            '\n${' ' * (indent + 4)}$provide'
            '$multilinePostfix;';
      } else {
        // No postfix, keep bind<Type>() with provide start
        return '${indentStr}bind<$returnType>()$provide;';
      }
    } else {
      // Simple multiline: break after bind<Type>()
      if (postfix.isNotEmpty) {
        final multilinePostfix = _generateMultilinePostfix(indent);
        return '${indentStr}bind<$returnType>()'
            '\n${' ' * (indent + 4)}$provide'
            '$multilinePostfix;';
      } else {
        return '${indentStr}bind<$returnType>()'
            '\n${' ' * (indent + 4)}$provide;';
      }
    }
  }

  // Internal method: decides how the provide clause should be generated by param kind.
  String _generateProvideClause(int indent) {
    if (hasParams) return _generateWithParamsProvideClause(indent);
    return _generatePlainProvideClause(indent);
  }

  /// Generates code when using runtime parameters (@params).
  String _generateWithParamsProvideClause(int indent) {
    // Safe variable name for parameters.
    const paramVar = 'args';
    final fnArgs = parameters.map((p) => p.generateArg(paramVar)).join(', ');
    // Use multiline format only if args are long or contain newlines
    final multiLine = fnArgs.length > 60 || fnArgs.contains('\n');
    switch (bindingType) {
      case BindingType.instance:
        throw StateError(
          'Internal error: _generateWithParamsProvideClause called for @instance binding with @params.',
        );
      //return isAsyncInstance
      //    ? '.toInstanceAsync(($fnArgs) => $methodName($fnArgs))'
      //    : '.toInstance(($fnArgs) => $methodName($fnArgs))';
      case BindingType.provide:
        if (isAsyncProvide) {
          return multiLine
              ? '.toProvideAsyncWithParams(\n${' ' * (indent + 2)}($paramVar) => $methodName($fnArgs))'
              : '.toProvideAsyncWithParams(($paramVar) => $methodName($fnArgs))';
        } else {
          return multiLine
              ? '.toProvideWithParams(\n${' ' * (indent + 2)}($paramVar) => $methodName($fnArgs))'
              : '.toProvideWithParams(($paramVar) => $methodName($fnArgs))';
        }
    }
  }

  /// Generates code when only resolved (not runtime) arguments used.
  String _generatePlainProvideClause(int indent) {
    final argsStr = parameters.map((p) => p.generateArg()).join(', ');

    // Check if we need multiline formatting based on total line length
    final singleLineCall = '$methodName($argsStr)';
    final needsMultiline =
        singleLineCall.length >= 45 || argsStr.contains('\n');

    switch (bindingType) {
      case BindingType.instance:
        return isAsyncInstance
            ? '.toInstanceAsync($methodName($argsStr))'
            : '.toInstance($methodName($argsStr))';
      case BindingType.provide:
        if (isAsyncProvide) {
          if (needsMultiline) {
            final lambdaIndent = (isSingleton || named != null)
                ? indent + 6
                : indent + 2;
            final closingIndent = (isSingleton || named != null)
                ? indent + 4
                : indent;
            return '.toProvideAsync(\n${' ' * lambdaIndent}() => $methodName($argsStr),\n${' ' * closingIndent})';
          } else {
            return '.toProvideAsync(() => $methodName($argsStr))';
          }
        } else {
          if (needsMultiline) {
            final lambdaIndent = (isSingleton || named != null)
                ? indent + 6
                : indent + 2;
            final closingIndent = (isSingleton || named != null)
                ? indent + 4
                : indent;
            return '.toProvide(\n${' ' * lambdaIndent}() => $methodName($argsStr),\n${' ' * closingIndent})';
          } else {
            return '.toProvide(() => $methodName($argsStr))';
          }
        }
    }
  }

  /// EN / RU: Adds .withName and .singleton if needed.
  String _generatePostfix() {
    final namePart = named != null ? ".withName('$named')" : '';
    final singletonPart = isSingleton ? '.singleton()' : '';
    return '$namePart$singletonPart';
  }

  /// EN / RU: Generates multiline postfix with proper indentation.
  String _generateMultilinePostfix(int indent) {
    final parts = <String>[];
    if (named != null) {
      parts.add(".withName('$named')");
    }
    if (isSingleton) {
      parts.add('.singleton()');
    }
    if (parts.isEmpty) return '';

    return parts.map((part) => '\n${' ' * (indent + 4)}$part').join('');
  }

  /// -------------------------------------------------------------------------
  /// fromMethod
  ///
  /// Constructs a [BindSpec] from an analyzer [MethodElement].
  ///
  /// Validates and parses all type annotations, method/parameter DI hints,
  /// and derives async and parametric flags as needed.
  ///
  /// ## Example
  /// ```dart
  /// final bindSpec = BindSpec.fromMethod(methodElement);
  /// print(bindSpec.returnType); // e.g., 'Logger'
  /// ```
  /// Throws [AnnotationValidationException] or [CodeGenerationException] if invalid.
  static BindSpec fromMethod(MethodElement2 method) {
    try {
      // Validate method annotations
      AnnotationValidator.validateMethodAnnotations(method);

      // Parse return type using improved type parser
      final parsedReturnType = TypeParser.parseType(method.returnType, method);

      final methodName = method.firstFragment.name2 ?? '';

      // Check for @singleton annotation.
      final isSingleton = MetadataUtils.anyMeta(
        method.firstFragment.metadata2.annotations,
        'singleton',
      );

      // Get @named value if present.
      final named = MetadataUtils.getNamedValue(
        method.firstFragment.metadata2.annotations,
      );

      // Parse each method parameter.
      final params = <BindParameterSpec>[];
      bool hasParams = false;
      for (final p in method.formalParameters) {
        final typeStr = p.type.getDisplayString();
        final paramNamed = MetadataUtils.getNamedValue(
          p.firstFragment.metadata2.annotations,
        );
        final isParams = MetadataUtils.anyMeta(
          p.firstFragment.metadata2.annotations,
          'params',
        );
        if (isParams) hasParams = true;
        params.add(BindParameterSpec(typeStr, paramNamed, isParams: isParams));
      }

      // Determine bindingType: @instance or @provide.
      final hasInstance = MetadataUtils.anyMeta(
        method.firstFragment.metadata2.annotations,
        'instance',
      );
      final hasProvide = MetadataUtils.anyMeta(
        method.firstFragment.metadata2.annotations,
        'provide',
      );

      if (!hasInstance && !hasProvide) {
        throw AnnotationValidationException(
          'Method must be marked with either @instance() or @provide() annotation',
          element: method,
          suggestion:
              'Add @instance() for direct instances or @provide() for factory methods',
          context: {
            'method_name': methodName,
            'return_type': parsedReturnType.displayString,
          },
        );
      }

      final bindingType = hasInstance
          ? BindingType.instance
          : BindingType.provide;

      // PROHIBIT @params with @instance bindings!
      if (bindingType == BindingType.instance && hasParams) {
        throw AnnotationValidationException(
          '@params() (runtime arguments) cannot be used together with @instance()',
          element: method,
          suggestion: 'Use @provide() instead if you want runtime arguments',
          context: {
            'method_name': methodName,
            'binding_type': 'instance',
            'has_params': hasParams,
          },
        );
      }

      // Set async flags based on parsed type
      final isAsyncInstance =
          bindingType == BindingType.instance && parsedReturnType.isFuture;
      final isAsyncProvide =
          bindingType == BindingType.provide && parsedReturnType.isFuture;

      return BindSpec(
        returnType: parsedReturnType.codeGenType,
        methodName: methodName,
        isSingleton: isSingleton,
        named: named,
        parameters: params,
        bindingType: bindingType,
        isAsyncInstance: isAsyncInstance,
        isAsyncProvide: isAsyncProvide,
        hasParams: hasParams,
      );
    } catch (e) {
      if (e is CherryPickGeneratorException) {
        rethrow;
      }
      throw CodeGenerationException(
        'Failed to create BindSpec from method "${method.displayName}"',
        element: method,
        suggestion:
            'Check that the method has valid annotations and return type',
        context: {
          'method_name': method.displayName,
          'return_type': method.returnType.getDisplayString(),
          'error': e.toString(),
        },
      );
    }
  }
}
