// Copyright (C) 2023 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:http/http.dart' as http;
import 'package:wyatt_i18n/src/core/enums/format.dart';
import 'package:wyatt_i18n/src/core/exceptions/exceptions.dart';
import 'package:wyatt_i18n/src/core/utils/assets_utils.dart';
import 'package:wyatt_i18n/src/core/utils/intl_utils.dart';
import 'package:wyatt_i18n/src/core/utils/parsers/locale_parser.dart';
import 'package:wyatt_i18n/src/domain/data_sources/i18n_data_source.dart';
import 'package:wyatt_i18n/src/domain/entities/i18n.dart';

/// {@template network_i18n_data_source_impl}
/// Implementation of [I18nDataSource] that loads i18n files from the network.
///
/// The [baseUri] is the base uri where the i18n files are located.
/// The [baseName] is the name of the i18n files without the extension.
/// The [fallbackAssetPath] is the path to the fallback i18n files.
/// The [format] is the format of the i18n files.
/// The [defaultLocale] is the default locale to use when the locale is not
/// specified.
/// The [separator] is the separator to use when the locale is specified.
///
/// For example, if the i18n files are located at `https://example.com/i18n/`
/// and are named `i18n.en.arb`, `i18n.fr.arb`, etc., then the [baseUri] is
/// `https://example.com/i18n/` and the [baseName] is `i18n` and the
/// [format] is [Format.arb].
///
/// If the i18n file for the given locale is not found or if the fetch fails,
/// then the fallback i18n files are loaded from the [fallbackAssetPath] in
/// the root bundle.
///
/// For example, if the fallback i18n files are located in the `assets/l10n/`
/// and are named `i18n.arb`, `i18n.en.arb`, `i18n.fr.arb`, etc., then the
/// [fallbackAssetPath] is `l10n`.
/// {@endtemplate}
class NetworkI18nDataSourceImpl extends I18nDataSource {
  /// {@macro network_i18n_data_source_impl}
  NetworkI18nDataSourceImpl({
    required this.baseUri,
    this.baseName = 'i18n',
    this.fallbackAssetPath = '',
    super.format = Format.arb,
    super.defaultLocale = const Locale('en'),
    super.separator = '.',
    super.localeTransformer,
  }) : super();

  /// Creates a new instance of [NetworkI18nDataSourceImpl] with the system
  /// locale as the default locale.
  /// So, the [defaultLocale] is the system locale and you don't need to
  /// specify it when you load the i18n file.
  // ignore: long-parameter-list
  static Future<NetworkI18nDataSourceImpl> withSystemLocale({
    required Uri baseUri,
    String baseName = 'i18n',
    Format format = Format.arb,
    String separator = '.',
    LocaleTransformer? localeTransformer,
  }) async {
    final locale = await IntlUtils.getSystemLocale();

    return NetworkI18nDataSourceImpl(
      baseUri: baseUri,
      baseName: baseName,
      format: format,
      defaultLocale: const LocaleParser().parse(locale),
      separator: separator,
      localeTransformer: localeTransformer,
    );
  }

  /// The base uri where the i18n files are located.
  final Uri baseUri;

  /// The name of the i18n files without the extension.
  final String baseName;

  /// The path to the fallback i18n files.
  ///
  /// This is used when the i18n file for the given locale is not found or
  /// if the fetch fails.
  ///
  /// Note: The path is relative to the root bundle. So `assets/l10n/` is
  /// `l10n`.
  final String fallbackAssetPath;

  /// Tries to load the i18n file from the given [locale].
  Future<I18n> _tryLoad(Locale? locale, {Uri? overrideUri}) async {
    String? content;
    final ext = format.name;

    final defaultLocaleString = localeTransformer?.call(defaultLocale) ??
        const LocaleParser().serialize(defaultLocale);

    final localeString = locale == null
        ? defaultLocaleString
        : localeTransformer?.call(locale) ??
            const LocaleParser().serialize(locale);

    /// If the locale is null, then we try to load the default i18n file from
    /// the fallback asset path.
    /// Otherwise, we try to load the i18n file for the given locale from the
    /// base uri.
    final path = AssetsUtils.cleanPath(
      locale == null
          ? '$fallbackAssetPath/$baseName.$defaultLocaleString.$ext'
          : overrideUri?.toString() ?? '$baseUri/$baseName.$localeString.$ext',
    );

    if (locale == null) {
      /// Load the default i18n file from the fallback asset path.
      try {
        if (await AssetsUtils.assetExists(path)) {
          content = await rootBundle.loadString(path);
        }
      } catch (_) {
        content = null;
      }
    } else {
      /// Load the i18n file for the given locale from the base uri.
      try {
        final response = await http.get(Uri.parse(path));
        if (response.statusCode >= 200 && response.statusCode < 300) {
          content = response.body;
        }
      } catch (_) {
        content = null;
      }
    }

    /// If the i18n file is not found, then we try to load the
    /// default i18n file.
    if (content == null && locale != null) {
      try {
        final fallbackPath = AssetsUtils.cleanPath(
          '$fallbackAssetPath/$baseName.$defaultLocaleString.$ext',
        );
        content = await rootBundle.loadString(fallbackPath);
      } catch (_) {
        throw SourceNotFoundException(path, format: format);
      }
    }

    /// If the default i18n file is not found, then we throw an exception.
    /// This case should happen only if the locale is null.
    if (content == null) {
      throw SourceNotFoundException(path, format: format);
    }

    /// Parse the i18n file.
    final parsedData = format.parser.parse(content);

    return I18n(
      unparsedData: content,
      data: parsedData,
      locale: locale,
    );
  }

  /// Loads the i18n file for the given [locale].
  ///
  /// If the [locale] is null, then the default i18n file is loaded from the
  /// fallback asset path.
  @override
  Future<I18n> load({Locale? locale}) async =>
      super.currentI18nFile = await _tryLoad(locale);

  /// Loads the i18n file from the given [uri].
  ///
  /// If the fetch fails, then the fallback i18n files are loaded from the
  /// [fallbackAssetPath] in the root bundle.
  @override
  Future<I18n> loadFrom(Uri uri) async =>
      super.currentI18nFile = await _tryLoad(null, overrideUri: uri);
}
