// Copyright (c) 2024, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:io';

import 'package:hooks/hooks.dart';

import 'code_asset.dart';
import 'config.dart';
import 'link_mode.dart';
import 'link_mode_preference.dart';
import 'os.dart';
import 'syntax.g.dart';

/// Validates the code asset specific parts of a [BuildInput].
Future<ValidationErrors> validateCodeAssetBuildInput(BuildInput input) async =>
    [
      ..._validateConfig('BuildInput.config.code', input.config),
      ...await _validateCodeAssetHookInput([
        for (final assets in input.assets.encodedAssets.values) ...assets,
      ]),
    ];

/// Validates the code asset specific parts of a [LinkInput].
Future<ValidationErrors> validateCodeAssetLinkInput(LinkInput input) async => [
  ..._validateConfig('LinkInput.config.code', input.config),
  ...await _validateCodeAssetHookInput(input.assets.encodedAssets),
];

ValidationErrors _validateConfig(String inputName, HookConfig config) {
  final syntaxErrors = _validateConfigSyntax(config);
  if (syntaxErrors.isNotEmpty) {
    return syntaxErrors;
  }

  final code = config.code;
  final errors = <String>[];
  final cCompiler = code.cCompiler;
  if (cCompiler != null) {
    errors.addAll([
      ..._validateFile('$inputName.cCompiler.compiler', cCompiler.compiler),
      ..._validateFile('$inputName.cCompiler.linker', cCompiler.linker),
      ..._validateFile('$inputName.cCompiler.archiver', cCompiler.archiver),
    ]);
    if (code.targetOS == OS.windows &&
        cCompiler.windows.developerCommandPrompt != null) {
      errors.addAll([
        ..._validateFile(
          '$inputName.cCompiler.windows.developerCommandPrompt.script',
          cCompiler.windows.developerCommandPrompt!.script,
        ),
      ]);
    }
  }
  return errors;
}

ValidationErrors _validateConfigSyntax(HookConfig config) {
  final syntaxNode = ConfigSyntax.fromJson(config.json, path: config.path);
  final syntaxErrors = syntaxNode.validate();
  if (syntaxErrors.isEmpty) {
    return [];
  }
  return [...syntaxErrors, _semanticValidationSkippedMessage(syntaxNode.path)];
}

Future<ValidationErrors> _validateCodeAssetHookInput(
  List<EncodedAsset> encodedAssets,
) async {
  final errors = <String>[];
  for (final asset in encodedAssets) {
    if (!asset.isCodeAsset) continue;
    final syntaxErrors = _validateCodeAssetSyntax(asset);
    if (syntaxErrors.isNotEmpty) {
      errors.addAll(syntaxErrors);
      continue;
    }
    errors.addAll(_validateCodeAssetFile(CodeAsset.fromEncoded(asset)));
  }
  return errors;
}

/// Validates the code asset specific parts of a [BuildOutput].
Future<ValidationErrors> validateCodeAssetBuildOutput(
  BuildInput input,
  BuildOutput output,
) => _validateCodeAssetBuildOrLinkOutput(
  input,
  input.config.code,
  output.assets.encodedAssets,
  [
    ...output.assets.encodedAssetsForBuild,
    ...output.assets.encodedAssetsForLinking.values.expand((assets) => assets),
  ],
  output,
  true,
);

/// Validates the code asset specific parts of a [LinkOutput].
Future<ValidationErrors> validateCodeAssetLinkOutput(
  LinkInput input,
  LinkOutput output,
) => _validateCodeAssetBuildOrLinkOutput(
  input,
  input.config.code,
  output.assets.encodedAssets,
  output.assets.encodedAssetsForLink.values.expand((assets) => assets),
  output,
  false,
);

/// Validates that the given code assets can be used together in an application.
///
/// Some restrictions - e.g. unique shared library names - have to be validated
/// on the entire application build and not on individual `hook/build.dart`
/// invocations.
Future<ValidationErrors> validateCodeAssetInApplication(
  List<EncodedAsset> assets,
) async {
  final codeAssets = <CodeAsset>[];
  for (final asset in assets) {
    if (!asset.isCodeAsset) continue;
    final codeAsset = CodeAsset.fromEncoded(asset);
    codeAssets.add(codeAsset);
  }
  final errors = <String>[];
  _validateNoDuplicateDylibNames(errors, codeAssets);
  _validateNoDuplicateAssetIds(errors, codeAssets);
  return errors;
}

Future<ValidationErrors> _validateCodeAssetBuildOrLinkOutput(
  HookInput input,
  CodeConfig codeConfig,
  Iterable<EncodedAsset> encodedAssetsBundled,
  Iterable<EncodedAsset> encodedAssetsNotBundled,
  HookOutput output,
  bool isBuild,
) async {
  final errors = <String>[];

  final codeAssetsBundled = <CodeAsset>[];
  for (final asset in encodedAssetsBundled) {
    if (!asset.isCodeAsset) continue;
    final syntaxErrors = _validateCodeAssetSyntax(asset);
    if (syntaxErrors.isNotEmpty) {
      errors.addAll(syntaxErrors);
      continue;
    }
    final codeAsset = CodeAsset.fromEncoded(asset);
    _validateCodeAsset(input, codeConfig, codeAsset, errors, isBuild, true);
    codeAssetsBundled.add(codeAsset);
  }
  _validateNoDuplicateDylibNames(errors, codeAssetsBundled);
  _validateNoDuplicateAssetIds(errors, codeAssetsBundled);

  final codeAssetsNotBundled = <CodeAsset>[];
  for (final asset in encodedAssetsNotBundled) {
    if (!asset.isCodeAsset) continue;
    final syntaxErrors = _validateCodeAssetSyntax(asset);
    if (syntaxErrors.isNotEmpty) {
      errors.addAll(syntaxErrors);
      continue;
    }
    final codeAsset = CodeAsset.fromEncoded(asset);
    _validateCodeAsset(input, codeConfig, codeAsset, errors, isBuild, false);
    codeAssetsNotBundled.add(codeAsset);
  }
  _validateNoDuplicateDylibNames(errors, codeAssetsNotBundled);
  _validateNoDuplicateAssetIds(errors, codeAssetsNotBundled);

  return errors;
}

ValidationErrors _validateCodeAssetSyntax(EncodedAsset encodedAsset) {
  if (!encodedAsset.isCodeAsset) {
    return [];
  }
  final syntaxNode = NativeCodeAssetEncodingSyntax.fromJson(
    encodedAsset.encoding,
    path: encodedAsset.encodingJsonPath ?? [],
  );
  final syntaxErrors = syntaxNode.validate();
  if (syntaxErrors.isEmpty) {
    return [];
  }
  return [...syntaxErrors, _semanticValidationSkippedMessage(syntaxNode.path)];
}

String _semanticValidationSkippedMessage(List<Object> jsonPath) {
  final pathString = jsonPath.join('.');
  return "Syntax errors in '$pathString'. Semantic validation skipped.";
}

void _validateCodeAsset(
  HookInput input,
  CodeConfig codeConfig,
  CodeAsset codeAsset,
  ValidationErrors errors,
  bool validateAssetId,
  bool validateLinkMode,
) {
  final id = codeAsset.id;
  final prefix = 'package:${input.packageName}/';
  if (validateAssetId && !id.startsWith(prefix)) {
    errors.add('Code asset "$id" does not start with "$prefix".');
  }

  if (validateLinkMode) {
    final preference = codeConfig.linkModePreference;
    final linkMode = codeAsset.linkMode;
    if ((linkMode is DynamicLoading &&
            preference == LinkModePreference.static) ||
        (linkMode is StaticLinking &&
            preference == LinkModePreference.dynamic)) {
      errors.add(
        'CodeAsset "$id" has a link mode "$linkMode", which '
        'is not allowed by by the input link mode preference '
        '"$preference".',
      );
    }
  }

  errors.addAll(_validateCodeAssetFile(codeAsset));
}

ValidationErrors _validateCodeAssetFile(CodeAsset codeAsset) {
  final id = codeAsset.id;
  final file = codeAsset.file;
  return [
    if (file == null && _mustHaveFile(codeAsset.linkMode))
      'CodeAsset "$id" has no file.',
    if (file != null) ..._validateFile('Code asset "$id" file', file),
  ];
}

bool _mustHaveFile(LinkMode linkMode) => switch (linkMode) {
  LookupInExecutable _ => false,
  LookupInProcess _ => false,
  DynamicLoadingSystem _ => false,
  DynamicLoadingBundled _ => true,
  StaticLinking _ => true,
  _ => throw UnsupportedError('Unknown link mode: $linkMode.'),
};

void _validateNoDuplicateDylibNames(
  ValidationErrors errors,
  List<CodeAsset> codeAssets,
) {
  final fileNameToAssets = <String, Set<CodeAsset>>{};
  for (final codeAsset in codeAssets) {
    final file = codeAsset.file;
    if (file != null) {
      final fileName = file.pathSegments.where((s) => s.isNotEmpty).last;
      fileNameToAssets[fileName] ??= {};
      fileNameToAssets[fileName]!.add(codeAsset);
    }
  }

  for (final fileName in fileNameToAssets.keys) {
    final assets = fileNameToAssets[fileName]!;
    if (assets.length > 1) {
      final assetIdsString = assets.map((e) => '"${e.id}"').join(', ');
      final error =
          'Duplicate dynamic library file name "$fileName" for the following'
          ' asset ids: $assetIdsString.';
      errors.add(error);
    }
  }
}

void _validateNoDuplicateAssetIds(
  ValidationErrors errors,
  List<CodeAsset> codeAssets,
) {
  final assetIdToAssets = <String, Set<CodeAsset>>{};
  for (final codeAsset in codeAssets) {
    final assetId = codeAsset.id;
    assetIdToAssets[assetId] ??= {};
    assetIdToAssets[assetId]!.add(codeAsset);
  }

  for (final assetIds in assetIdToAssets.values) {
    if (assetIds.length > 1) {
      final assetId = assetIds.first.id;
      final filesString = assetIds.map((e) => '"${e.file?.path}"').join(', ');
      final error =
          'Multiple assets with the same id: "$assetId". '
          'The duplicate assets have the following files: $filesString.';
      errors.add(error);
    }
  }
}

ValidationErrors _validateFile(
  String name,
  Uri uri, {
  bool mustExist = true,
  bool mustBeAbsolute = true,
}) {
  final errors = <String>[];
  if (mustBeAbsolute && !uri.isAbsolute) {
    errors.add(
      '$name (${uri.toFilePath()}) must be an absolute path. '
      'Prefer constructing it via `input.outputDirectoryShared`.',
    );
  }
  if (mustExist && !File.fromUri(uri).existsSync()) {
    errors.add('$name (${uri.toFilePath()}) does not exist as a file.');
  }
  return errors;
}
