import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

import 'package:logging/logging.dart';
import 'package:nc_photos/mobile/android/self_signed_cert.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/type.dart';
import 'package:path/path.dart' as path_lib;
import 'package:path_provider/path_provider.dart';
import 'package:uuid/uuid.dart';

part 'self_signed_cert_manager.g.dart';

@npLog
class SelfSignedCertManager {
  factory SelfSignedCertManager() => _inst;

  SelfSignedCertManager._() {
    _readAllCerts().then((infos) {
      _whitelist = infos;
    });
  }

  void init() {
    HttpOverrides.global = _CustomHttpOverrides();
  }

  /// Verify [cert] and return if it's registered in the whitelist for [host]
  bool verify(X509Certificate cert, String host, int port) {
    final fingerprint = _sha1BytesToString(cert.sha1);
    return _whitelist.any((info) =>
        fingerprint == info.sha1 &&
        host.toLowerCase() == info.host.toLowerCase());
  }

  String getLastBadCertHost() => _latestBadCert.host;

  String getLastBadCertFingerprint() =>
      _sha1BytesToString(_latestBadCert.cert.sha1);

  /// Whitelist the last bad cert
  Future<void> whitelistLastBadCert() async {
    final info = await _writeCert(_latestBadCert.host, _latestBadCert.cert);
    _whitelist.add(info);
    return SelfSignedCert.reload();
  }

  /// Clear all whitelisted certs and they will no longer be allowed afterwards
  Future<void> clearWhitelist() async {
    final certDir = await _openCertsDir();
    await certDir.delete(recursive: true);
    _whitelist.clear();
    return SelfSignedCert.reload();
  }

  /// Read and return all persisted certificate infos
  Future<List<_CertInfo>> _readAllCerts() async {
    final products = <_CertInfo>[];
    final certDir = await _openCertsDir();
    final certFiles = (await certDir.list().toList()).whereType<File>();
    for (final f in certFiles) {
      if (!f.path.endsWith(".json")) {
        continue;
      }
      try {
        final info = _CertInfo.fromJson(jsonDecode(await f.readAsString()));
        _log.info(
            "[_readAllCerts] Found certificate info: ${path_lib.basename(f.path)} for host: ${info.host}");
        products.add(info);
      } catch (e, stacktrace) {
        _log.severe(
            "[_readAllCerts] Failed to read certificate file: ${path_lib.basename(f.path)}",
            e,
            stacktrace);
      }
    }
    return products;
  }

  /// Persist a new cert and return the info object
  Future<_CertInfo> _writeCert(String host, X509Certificate cert) async {
    final certDir = await _openCertsDir();
    while (true) {
      final fileName = const Uuid().v4();
      final certF = File("${certDir.path}/$fileName");
      if (await certF.exists()) {
        continue;
      }
      await certF.writeAsString(cert.pem, flush: true);

      final siteF = File("${certDir.path}/$fileName.json");
      final certInfo = _CertInfo.fromX509Certificate(host, cert);
      await siteF.writeAsString(jsonEncode(certInfo.toJson()), flush: true);
      _log.info(
          "[_persistBadCert] Persisted cert at '${certF.path}' for host '${_latestBadCert.host}'");
      return certInfo;
    }
  }

  Future<Directory> _openCertsDir() async {
    final privateDir = await getApplicationSupportDirectory();
    final certDir = Directory("${privateDir.path}/certs");
    if (!await certDir.exists()) {
      return certDir.create();
    } else {
      return certDir;
    }
  }

  late _BadCertInfo _latestBadCert;
  var _whitelist = <_CertInfo>[];

  static final _inst = SelfSignedCertManager._();
}

// Modifications to this class must also reflect on Android side
class _CertInfo {
  _CertInfo(this.host, this.sha1, this.subject, this.issuer, this.startValidity,
      this.endValidity);

  factory _CertInfo.fromX509Certificate(String host, X509Certificate cert) {
    return _CertInfo(
      host,
      _sha1BytesToString(cert.sha1),
      cert.subject,
      cert.issuer,
      cert.startValidity,
      cert.endValidity,
    );
  }

  JsonObj toJson() {
    return {
      "host": host,
      "sha1": sha1,
      "subject": subject,
      "issuer": issuer,
      "startValidity": startValidity.toUtc().toIso8601String(),
      "endValidity": endValidity.toUtc().toIso8601String(),
    };
  }

  factory _CertInfo.fromJson(JsonObj json) {
    return _CertInfo(
      json["host"],
      json["sha1"],
      json["subject"],
      json["issuer"],
      DateTime.parse(json["startValidity"]),
      DateTime.parse(json["endValidity"]),
    );
  }

  final String host;
  final String sha1;
  final String subject;
  final String issuer;
  final DateTime startValidity;
  final DateTime endValidity;
}

class _BadCertInfo {
  _BadCertInfo(this.cert, this.host, this.port);

  final X509Certificate cert;
  final String host;
  final int port;
}

@npLog
class _CustomHttpOverrides extends HttpOverrides {
  @override
  HttpClient createHttpClient(SecurityContext? context) {
    return super.createHttpClient(context)
      ..badCertificateCallback = (cert, host, port) {
        try {
          if (SelfSignedCertManager().verify(cert, host, port)) {
            // _log.warning(
            //     "[badCertificateCallback] Allowing whitelisted self-signed cert");
            return true;
          }
        } catch (e, stacktrace) {
          _log.shout("[badCertificateCallback] Failed while verifying cert", e,
              stacktrace);
        }
        SelfSignedCertManager()._latestBadCert = _BadCertInfo(cert, host, port);
        return false;
      };
  }
}

String _sha1BytesToString(Uint8List bytes) =>
    bytes.map((e) => e.toRadixString(16).padLeft(2, "0")).join();