import 'dart:async';
import 'dart:core';
import 'dart:typed_data';

import 'package:app_business/app_config.dart';
import 'package:app_business/generated/base.pb.dart' as pb;
import 'package:app_business/generated/google/protobuf/duration.pb.dart' as pb;
import 'package:app_business/generated/google/protobuf/timestamp.pb.dart' as pb;
import 'package:app_business/generated/track_offical.pbgrpc.dart' as pb;
import 'package:app_business/service/app.dart';
import 'package:grpc/grpc.dart';
import 'package:track_common/model.dart';
import 'package:track_common/track_common.dart';

import 'abase.dart';

export 'package:app_business/generated/base.pb.dart'
    show DefaultRequest, IdRequest, IdArrRequest;
export 'package:app_business/generated/google/protobuf/timestamp.pb.dart';
export 'package:app_business/generated/track_offical.pbgrpc.dart'
    show ToMatchRegusterAddRequest, ToMatchRegusterListRequest;
export 'package:fixnum/fixnum.dart' show Int64;

typedef SmsType = pb.SmsType;

extension _ImageExt on pb.NetImage {
  NetImage toModel() {
    return NetImage(url, md5);
  }
}

extension DateTimeExt on DateTime? {
  pb.Timestamp toPb() {
    if (this != null) {
      return pb.Timestamp.fromDateTime(this!.toUtc());
    } else {
      return pb.Timestamp();
    }
  }
}

extension TSExt on pb.Timestamp {
  DateTime toModel() {
    return toDateTime(toLocal: true);
  }
}

extension DurationExt on pb.Duration {
  Duration toModel() {
    final microseconds = hasNanos() ? nanos / 1000 : 0;
    return Duration(
        seconds: seconds.toInt(), microseconds: microseconds.toInt());
  }
}

extension PositionExt on pb.Position {
  Position toModel() {
    return Position(latitude: latitude, longitude: longitude)
      ..altitude = altitude;
  }
}

extension PBToControlPointExt on pb.ToControlPoint {
  ControlPoint toModel() {
    final one = ControlPoint()
      ..areaId = sn
      ..intId = id
      ..isSuccess = isCheckSuccess
      ..sn = orderNo.toString()
      ..checkAfterPrev = checkAfterLast.toModel()
      ..checkAfterStart = checkAfterStart.toModel()
      ..checkDistanceAfterPrev = disAfterLast.meter
      ..distanceStraightToPrev = disStraightAfterLast.meter
      ..paceAfterPrev = Pace.perKm(paceAfterLast.seconds)
      ..paceAfterStart = Pace.perKm(paceAfterStart.seconds)
      ..position = ciPosition.toModel();

    if (cType == pb.CType.BeginType) {
      one.isStart = true;
    }
    if (cType == pb.CType.EndType) {
      one.isFinish = true;
    }

    return one;
  }
}

class ApiService extends IService {
  ClientChannel? channel;

  @override
  Future<void> init() async {
    channel = _newChannel();
  }

  ClientChannel _newChannel() {
    return ClientChannel(
      AppConfig.apiHost,
      port: AppConfig.apiPort,
      options: const ChannelOptions(credentials: ChannelCredentials.secure()
          // ChannelCredentials.insecure()
          ),
    );
  }

  pb.ApiToClient get stub {
    return _newStub(timeout: 10.seconds);
  }

  pb.ApiToClient _newStub({Duration? timeout, ClientChannel? channel}) {
    if (this.channel == null) {
      throw Exception('$runtimeType 未初始化');
    }
    final metadata = <String, String>{};
    metadata['version'] = _appVersion;
    if (token != null) {
      metadata['token'] = token!;
    }
    // debug("token: $token");

    return pb.ApiToClient(channel ?? this.channel!,
        options: CallOptions(
          metadata: metadata,
          timeout: timeout,
        ));
  }

  String get _appVersion => Get.find<AppService>().appVersion;

  set token(String? v) {
    Get.find<AppService>().token.val = v ?? '';
  }

  String? get token {
    final app = Get.find<AppService>();
    final token = app.token.val;
    if (token.isEmpty) {
      return null;
    }
    return token;
  }

  // 获取短信验证码
  Future<void> authSendCodeToPhone(String phone, SmsType smsType) async {
    info('authSendCodeToPhone [$phone]');
    await stub.toSendCodeToPhoneV2(pb.ToSendCodeToPhoneRequestV2()
      ..phone = phone
      ..smsType = smsType);
  }

  // 场控端_登录
  Future<void> signIn(String userCode, String password, String ip) async {
    final r = await stub.toSignInV2(pb.ToSignInRequestV2()
      ..userCode = userCode
      ..password = password
      ..ip = ip);
    token = r.token;

    debug('sign in success: $token');
  }

  // 场控端_登出
  void signOut() {
    stub.toSignOutV2(pb.DefaultRequest());
    token = null;
  }

  Future<Duration> getSmsSendLeftTime(String phone) async {
    final r = await stub
        .toGetSmsSendLeftTimeV2(pb.GetSmsSendLeftTimeRequest()..phone = phone);
    info('getSmsSendLeftTime: $phone - ${r.second}s');
    return r.second.seconds;
  }

  Future<List<MapInfo>> getMapList(int limit, int offset) async {
    final r = await stub.toMapListV2(pb.MapListRequestV2()
      ..limit = limit
      ..offset = offset);

    return r.list
        .map((e) => MapInfo(e.mapId, e.name, e.distance.meter, e.description,
            e.mapScaleNumber, e.image.toModel()))
        .toList();
  }

  Future<BinReader> getBinReaderByMd5(Uint8List md5) async {
    final stream =
        stub.toGetBinaryByMd5(pb.ToGetBinaryByMd5Request()..md5 = md5);
    final controller = StreamController<List<int>>();
    controller.onCancel = () {
      stream.cancel();
    };

    Future<void> rcv() async {
      try {
        await for (final one in stream) {
          controller.add(one.data);
        }
      } finally {
        controller.close();
        stream.cancel();
      }
    }

    rcv();
    stream.headers.then((value) => debug(value));
    final headers = await stream.headers;
    final lenStr = headers['all-length']!;
    final length = int.parse(lenStr);
    final nonce = headers['nonce']!;
    final ext = headers['ext']!;

    return BinReader(
        data: controller.stream, length: length, ext: ext, nonce: nonce);
  }

  Future<void> eventEdit(int id, EventRegisterInfo event) async {
    await stub.toMatchRegusterEdit(pb.ToMatchRegusterEditRequest()
      ..id = id
      ..regName = event.name
      ..startAt = event.startAt.toPb()
      ..stopAt = event.stopAt.toPb()
      ..isQueryPwd = event.password != null
      ..queryPasswd = event.password ?? '');
  }

  Future<void> eventUserAllocRoute(int checkId, int routeId) async {
    await stub.toCourseChackMatch(pb.ToCourseChackMatchRequest()
      ..id = checkId
      ..cId = routeId);
  }

  Future<void> eventUserAllocRouteAll(Iterable<int> checkIdList) async {
    await stub
        .allCourseChackMatch(pb.IdArrRequest()..idArr.addAll(checkIdList));
  }

  Future<void> eventUserStartAll(Iterable<int> checkIdList) async {
    await stub
        .allMatchChackInsStart(pb.IdArrRequest()..idArr.addAll(checkIdList));
  }

  Future<void> eventUserRestartAll(Iterable<int> checkIdList) async {
    await stub.allMatchChackInsForceResume(
        pb.IdArrRequest()..idArr.addAll(checkIdList));
  }

  Future<void> eventUserDelAll(Iterable<int> checkIdList) async {
    await stub
        .allMatchChackInsDel(pb.IdArrRequest()..idArr.addAll(checkIdList));
  }
}

class EventRegisterInfo {
  var id = 0;
  var name = '';
  var startAt = DateTime.now();
  var stopAt = DateTime.now();
  String? password;
}
