import 'dart:developer';
import 'dart:io';

//import 'package:dio/adapter.dart';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:cookie_jar/cookie_jar.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_flutter/hive_flutter.dart';
// import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../api/dio_exceptions.dart';
import 'package:html/dom.dart' as dom;
import 'package:intl/intl.dart';
import 'package:html/parser.dart';
// import 'package:path_provider/path_provider.dart';
import 'package:syncfusion_flutter_pdf/pdf.dart';

// import 'dio_logger.dart';

final crewlinkapiProvider = Provider<CrewlinkApi>((ref) {
  return CrewlinkApi();
});

class CrewlinkApi {
  static int maxDaysRoster = 59;
  static int maxDaysCrewlist = 30;

  // static final CrewlinkApi instance = CrewlinkApi._();
  late Dio _dio;

  String realUrl = "https://crewlink.flytunisair.net/crewlink";
  String get baseUrl => realUrl;
  Map<String, dynamic> get _headers => {
        "Host": Uri.parse(realUrl).host,
        "User-Agent":
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:103.0) Gecko/20100101 Firefox/103.0",
        "Accept":
            "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.5",
        "Accept-Encoding": "gzip, deflate, br",
        "Origin": Uri.parse(realUrl).host,
        "Connection": "keep-alive",
      };

  // CrewlinkApi._() {
  CrewlinkApi() {
    {
      _dio = Dio();

      //ignore bad certificate

      _dio.httpClientAdapter = IOHttpClientAdapter(
        createHttpClient: () {
          // Don't trust any certificate just because their root cert is trusted.
          final HttpClient client =
              HttpClient(context: SecurityContext(withTrustedRoots: false));
          // You can test the intermediate / root cert here. We just ignore it.
          client.badCertificateCallback = (cert, host, port) => true;
          return client;
        },
      );

      //default param
      _dio = _dio
        ..options.baseUrl = baseUrl
        ..options.followRedirects = false
        ..options.connectTimeout = const Duration(seconds: 5000)
        ..options.receiveTimeout = const Duration(seconds: 10000)
        ..options.responseType = ResponseType.plain
        ..options.validateStatus = (status) {
          return (status ?? 999) < 500;
        };

      //addcookie manager
      _dio.interceptors.add(CookieManager(CookieJar()));

      //add logger
      // _dio.interceptors.add(DioLogger(
      //     baseurl: false,
      //     path: true,
      //     statuscode: true,
      //     reqdata: true,
      //     resdata: true));
      //error interceptor

      //add header interceptor
      _dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) {
        options.headers.addAll(_headers);
        if (options.method == "POST") {
          options.headers
              .addAll({"Content-Type": "application/x-www-form-urlencoded"});
        }
        return handler.next(options); //continue
      }, onResponse: (response, handler) {
        return handler.next(response); // continue
      }, onError: (DioException e, handler) {
        // Do something with response error
        log("crewlinkapi: onerror: ${e.type} ${e.message}");
        return handler.resolve(Response(
            statusCode: e.response?.statusCode,
            statusMessage: DioExceptions.fromDioException(e).message,
            data: e.response?.data ?? "",
            headers: e.response?.headers,
            requestOptions: e.requestOptions));
      }));
    }
  }

  Future login({String? username, String? password}) async {
    storedusername = username ?? storedusername;
    storedpassword = password ?? storedpassword;
    var res = await _dio.get(
      "/crewlink.jsp?crewlinkOperation=crewlinkForCrew&resetSession=Y",
    );

    res = await _dio.post(
      '/clApp',
      data: {
        "crewlinkService": "crewlinkForCrew",
        "crewlinkOperation": "loadMainFrameSet",
        "crewlinkSourcePage": "spStartup",
        "crewlinkUserName": storedusername,
        "crewlinkPassword": storedpassword
      },
    );
    String? error;

    if (res.statusCode == 302) {
      error = "invalid username or password";
    } else if (res.statusCode == 200) {
      error = null;
    } else {
      error = res.statusMessage;
    }

    return {
      'error': error,
      'data': {"logged": (res.statusCode == 200)},
      'meta': {
        "user": username,
        "pass": password,
        "logged": (res.statusCode == 200),
        "statuscode": res.statusCode
      }
    };
  }

  Future rosterMinMax({bool loginfirst = false}) async {
    if (loginfirst) {
      var x = await login();
      if ((x["data"]["logged"] ?? false) == false) return x;
    }
    var res = await _dio.get(
      "/clApp?crewlinkService=individualDutyPlan&crewlinkOperation=default&crewlinkSourcePage=spCrew",
    );

    var mindate = (RegExp(
            r"(?<=minDateMinus1 = getDateFromFormat\(').+?(?=', 'yyyy-MM-dd'\);)",
            dotAll: true)
        .stringMatch(res.data ?? ""));

    var maxdate = (RegExp(
            r"(?<=maxDatePlus1 = getDateFromFormat\(').+?(?=', 'yyyy-MM-dd'\);)",
            dotAll: true)
        .stringMatch(res.data ?? ""));

    rostermin = mindate ?? "";
    rostermax = maxdate ?? "";

    String? error;
    if (res.statusCode == 302) {
      if (!loginfirst) return await rosterMinMax(loginfirst: true);
    } else if (res.statusCode == 200) {
      error = null;
    } else {
      error = res.statusMessage;
    }
    return {
      'error': error,
      'data': {"mindate": mindate, "maxdate": maxdate},
      'meta': {
        "user": storedusername,
        "pass": storedpassword,
        "statuscode": res.statusCode
      }
    };
  }

  Future roster(
      {required String start,
      required String end,
      required String fileid,
      CancelToken? cancelToken,
      ProgressCallback? progressCallback,
      bool loginfirst = false}) async {
    if (loginfirst) {
      var x = await login();
      if ((x["data"]["logged"] ?? false) == false) return x;
    }
    var res = await _dio.post("/clApp", data: {
      "crewlinkService": "individualDutyPlan",
      "crewlinkOperation": "makeReport",
      "buddyName": "",
      "beginDate": start,
      "endDate": end
    });

    String? pdflink =
        (RegExp(r'(?<=file=\/crewlink)(.+\.pdf?)(?=")', dotAll: true)
            .stringMatch(res.data ?? ""));
    String? msg = parse(res.data).querySelector(".errMsg")?.text.trim();
    //String fileid ="${(await getApplicationDocumentsDirectory()).path}/${DateFormat("yyyyMMddHHmm").format(DateTime.now())}_${storedusername}_${start}_$end.pdf";
    // String fileid =
    //     "${(await getApplicationDocumentsDirectory()).path}/ROSTER_${storedusername}_${start}_$end.pdf";

    String? error;
    if (res.statusCode == 302) {
      if (!loginfirst) {
        return await roster(
            start: start,
            end: end,
            loginfirst: true,
            fileid: fileid,
            cancelToken: cancelToken,
            progressCallback: progressCallback);
      } else {
        return "invalid username or password";
      }
    } else if (res.statusCode == 200 && pdflink != null) {
      error = null;

      await _dio.download(pdflink, fileid,
          cancelToken: cancelToken, onReceiveProgress: progressCallback);
    } else if (res.statusMessage != "") {
      error = res.statusMessage;
    } else {
      error = null;
    }
    return {
      'error': error,
      'data': {
        "pdflink": pdflink,
        "msg": msg,
        ...pdflink != null ? {"id": fileid} : {},
        ...pdflink != null ? {"decoded": _decodeRoster(fileid)} : {}
      },
      'meta': {
        "user": storedusername,
        "pass": storedpassword,
        "statuscode": res.statusCode
      }
    };
  }

  Future showCrewMinMax({bool loginfirst = false}) async {
    if (loginfirst) {
      var x = await login();
      if ((x["data"]["logged"] ?? false) == false) return x;
    }
    var res = await _dio.get(
        "/clApp?crewlinkService=showCrew&crewlinkOperation=default&crewlinkSourcePage=spCrew");

    var mindate = (RegExp(
            r"(?<=minDateMinus1 = getDateFromFormat\(').+?(?=', 'yyyy-MM-dd'\);)",
            dotAll: true)
        .stringMatch(res.data));

    var maxdate = (RegExp(
            r"(?<=maxDatePlus1 = getDateFromFormat\(').+?(?=', 'yyyy-MM-dd'\);)",
            dotAll: true)
        .stringMatch(res.data));

    showcrewmin = mindate ?? "";
    showcrewmax = maxdate ?? "";

    String? error;
    if (res.statusCode == 302) {
      if (!loginfirst) return await showCrewMinMax(loginfirst: true);
    } else if (res.statusCode == 200) {
      error = null;
    } else {
      error = res.statusMessage;
    }
    return {
      'error': error,
      'data': {"mindate": mindate, "maxdate": maxdate},
      'meta': {
        "user": storedusername,
        "pass": storedpassword,
        "statuscode": res.statusCode
      }
    };
  }

  Future showCrew(
      {required String start,
      required String end,
      bool crewlist = false,
      String al = "",
      String fnum = "",
      String dep = "",
      String des = "",
      bool loginfirst = false}) async {
    //String cachekey = "|$start,$end,$al,$fnum,$dep,$des,$crewlist";
    //start format ddMMMyy

    if (loginfirst) {
      var x = await login();
      if ((x["data"]["logged"] ?? false) == false) return x;
    }

    var res = await showCrewMinMax();
    if (res["error"] != null) return res;

    res = await _dio.post('/clApp', data: {
      "buddyName": "",
      "depDatePeriodBegin": start,
      "depDatePeriodEnd": end,
      "fnAirline": al,
      "fnNumber": fnum,
      "depAp": dep,
      "arrAp": des,
      "crewlinkService": "showCrew",
      "crewlinkOperation": "showCrew",
      "crewlinkSourcePage": "spCrew"
    });

    var msg = parse(res.data).querySelector(".errMsg")?.text;
    List leglist = [];
    if (res.statusCode == 200 && (msg ?? "") == "") {
      res = await _dio
          .get("/clApp?crewlinkService=showCrew&crewlinkOperation=legList");
      leglist = parse(res.data)
          .querySelectorAll("table tbody tr")
          .map((dom.Element tr) => (tr.children
              .map((td) => ((parse(td.innerHtml)
                      .querySelector('[name="indexOfLeg"]')
                      ?.attributes["value"]
                      .toString()
                      .trim()) ??
                  td.text.trim()))
              .toList()))
          .toList();
// treat cabin: cockpit: DH Flight:
      leglist = leglist.map((x) {
        x[7] =
            "${(RegExp(r"Cockpit: (.*?)(DH Flight: |Cabin: |$)").firstMatch(x[7])?.group(1) ?? "").trim()}|${(RegExp(r"Cabin: (.*?)(DH Flight: |$)").firstMatch(x[7])?.group(1) ?? "").trim()}|${(RegExp(r"DH Flight: (.*?)$").firstMatch(x[7])?.group(1) ?? "").trim()}";
        return x;
      }).toList();

      if (crewlist) {
        var out = [];
        for (var leg in leglist) {
          await _dio.post("/clApp", data: {
            "crewlinkService": "showCrew",
            "crewlinkOperation": "clearCrewList",
            "crewlinkSourcePage": ""
          });

          var res = await _dio.post("/clApp", data: {
            "crewlinkService": "showCrew",
            "crewlinkOperation": "exploreFlight",
            "crewlinkSourcePage": "",
            "indexOfLeg": leg[0].toString()
          });
          // log("flt: ${res.data}");

          var flt =
              ((parse(res.data).querySelector("table tr th[colspan]")?.text) ??
                      "\n\n\n\n\n\n\n")
                  .split("\n")
                  .skip(1)
                  .map((e) => e.trim())
                  .toList();
          // ..removeAt(6);

          var crew = [];
          if ((parse(res.data).querySelector(".errMsg")?.text != null)) {
            crew = [];
          } else {
            crew = (parse(res.data)
                    .querySelectorAll("table tr")
                    .map((e) =>
                        e.text.split("\n").map((e) => e.trim()).skip(1).toList()
                          ..removeAt(6))
                    .skip(2))
                .toList();
          }
          // log("crew: $crew");
          out.add([
            leg[0],
            flt[0],
            flt[1],
            flt[2],
            flt[3],
            flt[4],
            flt[5],
            leg[7],
            crew
          ]);
          //log((flt).toString());
        }
        leglist = out;
      }
    }

    String? error;
    if (res.statusCode == 302) {
      if (!loginfirst) {
        return await showCrew(
            start: start,
            end: end,
            crewlist: crewlist,
            al: al,
            fnum: fnum,
            dep: dep,
            des: dep,
            loginfirst: true);
      }
    } else if (res.statusCode == 200) {
      error = null;
    } else {
      error = res.statusMessage;
    }
    return {
      'error': error,
      'data': {
        "leglist": leglist,
        "msg": msg,
      },
      'meta': {
        "user": storedusername,
        "pass": storedpassword,
        "statuscode": res.statusCode
      }
    };
  }

  Future userInfo({bool loginfirst = false}) async {
    if (loginfirst) {
      var x = await login();
      if ((x["data"]["logged"] ?? false) == false) return x;
    }
    var res = await _dio.get("/HeaderPage.jsp");

    var data = (RegExp(
            r'(?<=\<td class="userInfo">)\s*<b>(.*?)<\/b>\s*(.+?)<br\/>\s*(.*?)\s(.{3})\s(.{3}).*?(?=\<\/td\>)',
            dotAll: true)
        .firstMatch(res.data));

    bool novalidsession = (res.data).indexOf("unknown user") > -1;

    String? error;
    if (novalidsession) {
      if (!loginfirst) return await userInfo(loginfirst: true);
    } else if (res.statusCode == 200) {
      error = null;
    } else {
      error = res.statusMessage;
    }

    return {
      'error': error,
      'data': {
        "tlc": data?.group(1),
        "name": data?.group(2),
        "qualif": (data?.group(3) ?? "").split(","),
        "base": data?.group(4),
        "sit": data?.group(5),
      },
      'meta': {"user": storedusername, "pass": storedpassword}
    };
  }

  Future changePass({required String newpass, bool loginfirst = false}) async {
    if (loginfirst) {
      var x = await login();
      if ((x["data"]["logged"] ?? false) == false) return x;
    }
    var res = await _dio.get(
        "/clApp?crewlinkService=changePassword&crewlinkOperation=default&crewlinkSourcePage=spCrew");

    res = await _dio.post("/clApp", data: {
      "oldPassword": storedpassword,
      "newPassword1": newpass,
      "newPassword2": newpass,
      "crewlinkService": "changePassword",
      "crewlinkOperation": "changePassword",
      "change": "Change"
    });
    var infomsg = parse(res.data).querySelector(".infoMsg")?.text;
    var msg = parse(res.data).querySelector(".errMsg")?.text;
    var errormsg = parse(res.data).querySelector("h1.error")?.text;

    String? error;
    if (res.statusCode == 302) {
      if (!loginfirst) {
        return await changePass(newpass: newpass, loginfirst: true);
      }
    } else if (res.statusCode == 200) {
      error = null;
    } else {
      error = res.statusMessage;
    }
    return {
      'error': error,
      'data': {"info": infomsg, "msg": msg, "error": errormsg},
      'meta': {
        "user": storedusername,
        "pass": storedpassword,
        "statuscode": res.statusCode
      }
    };
  }

  Future logout() async {
    var res = await _dio.get("/closeSession.jsp");
    storedusername = "";
    storedpassword = "";
    String? error;
    if (res.statusCode == 200) {
      error = null;
    } else {
      error = res.statusMessage;
    }
    log(res.toString());
    return {
      'error': error,
      'data': {},
      'meta': {
        "user": storedusername,
        "pass": storedpassword,
        "statuscode": res.statusCode
      }
    };
  }

  Future notif(
      {bool download = false,
      required String fileid,
      CancelToken? cancelToken,
      ProgressCallback? progressCallback,
      bool loginfirst = false}) async {
    if (loginfirst) {
      var x = await login();
      if ((x["data"]["logged"] ?? false) == false) return x;
    }
    var res = await _dio.get(
        "/clApp?crewlinkService=notification&crewlinkOperation=default&crewlinkSourcePage=spCrew");
    String? pdflink = (RegExp(
            r"(?<='js\/pdfjs\/web\/viewer\.html\?file=).+.pdf?(?=')",
            dotAll: true)
        .stringMatch(res.data));

    String? msg = parse(res.data).querySelector(".msgDetails")?.text;
    // String fileid =
    //     "${(await getApplicationDocumentsDirectory()).path}/${DateFormat("yyyyMMddHHmm").format(DateTime.now())}_${storedusername}_notif.pdf";

    String? error;
    if (res.statusCode == 302) {
      if (!loginfirst) {
        return await notif(
            download: download,
            fileid: fileid,
            cancelToken: cancelToken,
            progressCallback: progressCallback,
            loginfirst: true);
      }
    } else if (res.statusCode == 200) {
      error = null;
      if (download && pdflink != null) {
        await _dio.download(
          "${Uri.parse(realUrl).scheme}://${Uri.parse(realUrl).host}$pdflink",
          fileid,
        );
      }
    } else {
      error = res.statusMessage;
    }
    return {
      'error': error,
      'data': {
        "pdflink": pdflink,
        "msg": msg,
        ...pdflink != null ? {"id": fileid} : {}
      },
      'meta': {
        "user": storedusername,
        "pass": storedpassword,
        "statuscode": res.statusCode
      }
    };
  }

  Future confirmNotif({bool loginfirst = false}) async {
    if (loginfirst) {
      var x = await login();
      if ((x["data"]["logged"] ?? false) == false) return x;
    }

    var res = await _dio.post("/clApp", data: {
      'crewlinkService': 'notification',
      'crewlinkOperation': 'confirmNotif',
    });

    var msg = parse(res.data).querySelector(".errMsg")?.text;
    var errormsg = parse(res.data).querySelector("h1.error")?.text;

    String? error;
    if (res.statusCode == 302) {
      if (!loginfirst) return await confirmNotif(loginfirst: true);
    } else if (res.statusCode == 200) {
      error = null;
    } else {
      error = res.statusMessage;
    }
    // print("==============");
    // print({
    //   'error': error,
    //   'data': {"msg": msg, "error": errormsg},
    //   'meta': {
    //     "user": storedusername,
    //     "pass": storedpassword,
    //     "statuscode": res.statusCode
    //   }
    // });
    // print("==============");

    return {
      'error': error,
      'data': {"msg": msg, "error": errormsg},
      'meta': {
        "user": storedusername,
        "pass": storedpassword,
        "statuscode": res.statusCode
      }
    };
  }

  bool credSaved() {
    return (Hive.box("settings").get("crewlink_user", defaultValue: "") !=
            "") &&
        (Hive.box("settings").get("crewlink_pass", defaultValue: "") != "");
  }

  String storedusername = "";
  String storedpassword = "";
  bool get logged => storedusername != "" && storedpassword != "";
  String rostermin = "";
  String rostermax = "";
  String showcrewmin = "";
  String showcrewmax = "";

  Map<String, dynamic> decodeRoster(String pdffile) {
    return _decodeRoster(pdffile);
  }

  Map<String, dynamic> _decodeRoster(String pdffile) {
    // try {
    const weekdays = "Mon|Tue|Wed|Thu|Fri|Sat|Sun";
    const monthday = '[0-3]\\d';
    const month = "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec";
    const date = "$monthday($month)[0-5]\\d";
    const station = "[A-Z]{3}";
    const al = "[A-Z]{2}";
    const fnum = "\\d{1,4}[A-Z^R]?";
    const actype = "\\d{2}[A-Z0-9]";
    const label = "(OFF|[A-Z]{2,}|[A-Z]+([0-9?])|[A-Z]+/[A-Z]+[0-9]?)";
    const time = "[0-2]\\d[0-5]\\d";

    PdfDocument document =
        PdfDocument(inputBytes: File(pdffile).readAsBytesSync());
    var text = PdfTextExtractor(document).extractText(layoutText: true);
    document.dispose();

    text = text.replaceAll("\r\n", "#");

    // var text = ros.replaceAll("\n", "#");

    Map info = {};
    var d = RegExp(r'for(\d+)(.+?),(.+?)#').firstMatch(text);
    info["tlc"] = d?.group(1);
    info["lname"] = d?.group(2);
    info["fname"] = d?.group(3);
    d = RegExp(
            r'NetLine/Crew\((.+?)\)printedbyCREWLINK(.{7})(\d\d:\d\d)Page\d+#')
        .firstMatch(text);
    info["date"] = d?.group(2);
    info["time"] = d?.group(3);
    d = RegExp(r'Period:(.{7})-(.{7})#').firstMatch(text);
    info["startdate"] = d?.group(1);
    info["enddate"] = d?.group(2);

    String header =
        RegExp("(?<=Period:$date-$date#)(.*?)(?=dateHdutyRdeparrACinfo)")
                .stringMatch(text) ??
            "";
    String body = text.replaceAll(
        RegExp(
            "Individualdutyplan(.+?)${RegExp.escape(header)}(dateHdutyRdeparrACinfo#dateHdutyRdeparrACinfo#dateHdutyRdeparrACinfo#)?"),
        "");
    //   String footer = RegExp("(Flighttime.*)\$").stringMatch(body) ?? "";
    String onlyprog = RegExp("^(.*?)(?=Flighttime)").stringMatch(body) ?? "";
    onlyprog = onlyprog.replaceAllMapped(
        RegExp("(?:$weekdays)$monthday.+?(?=(($weekdays)$monthday)|\$)"),
        (m) => "${m.group(0)}\n");
    const ground =
        "(?<label>(?:SR|$label(?=R)|$label)?)(?<req>R)?(?<dep>$station)(?<hdep>$time)(?<hdes>$time)(?<actype>$actype?)?";

    const checkin = "C/I(?<station>$station)(?<time>$time)";
    const checkout = "C/O";
    const checkoutData = "(?<time>$time)(?<station>$station)";
    const flight =
        "(?<al>$al?)(?<fnum>$fnum?)(?<fltday>\\/$monthday)?(?<req>R)?(?<dep>$station?)(?<hdep>$time?)(?<hdes>$time?)(?<des>$station?)(?<actype>$actype?)";
    const dhflight =
        "DH\\/(?<al>$al?)(?<fnum>$fnum?)(?<fltday>\\/$monthday)?(?<req>R)?(?<dep>$station?)(?<hdep>$time?)(?<hdes>$time?)(?<des>$station?)";
    const dhlimo =
        "(?<label>$label?)(?<req>R)?(?<dep>$station?)(?<hdep>$time?)(?<hdes>$time?)(?<des>$station?)";
    const credit = "\\[Credit(?<credit>\\d+\\:\\d{2}?)\\]";
    const wholeday = "(?<label>$label)(?<req>R)?(?<dep>$station)";
    const changed = "CHANGED";

    var linedate = _ddmmmyy2date(info["startdate"]);
    Map<String, dynamic> liste = {
      "changed": changed,
      "checkin": checkin,
      "flight": flight,
      "dhflight": dhflight,
      "dhlimo": dhlimo,
      "ground": ground,
      "checkout": checkout,
      "credit": credit,
      "wholeday": wholeday,
    };
    Map<String, List<Map<String, dynamic>>> roster = {};

    for (var line in onlyprog.split("\n")..remove("")) {
      // log("... decoding $line");
      // var dateblock = RegExp("(($weekdays)$monthday)").stringMatch(line);
      // log(dateblock.toString());
      while (roster.keys.contains(_date2ymd(linedate))) {
        linedate = linedate.add(const Duration(days: 1));
      }

      var details = line.replaceAll(RegExp("($weekdays)$monthday"), "");
      // log("... decoding $details");

      List<Map<String, dynamic>> dayduty = [];
      //log(details);
      var elements = details.split("#");
      // log("${elements.length} $elements");
      for (var i = 0;
          (i < elements.length - 1) ||
              (i < elements.length && elements.last != "");
          i++) {
        var e = elements[i];
        bool decoded = false;
        String? remark;
        // log("_______ $i __el: $e");
        for (var dutytype in liste.keys) {
          if (RegExp(liste[dutytype]).hasMatch(e)) {
            late RegExpMatch? res;
            if (dutytype == "checkout" && i + 1 < elements.length) {
              res = RegExp(checkoutData).firstMatch(elements[i + 1]);
              i++;
            } else {
              res = RegExp(liste[dutytype]).firstMatch(e);
            }
            if (["flight", "dhflight", "ground", "dhlimo"].contains(dutytype) &&
                i + 1 < elements.length &&
                !liste.values.any((r) => RegExp(r).hasMatch(elements[i + 1]))) {
              remark = elements[i + 1];
              i++;
            }
            // log("_________$dutytype ${res?.groupNames.map((name) => "$name: ${res?.namedGroup(name)}")}");
            dayduty.add({
              "type": dutytype,
              "data": res?.groupNames.toList().fold(
                      {},
                      (Map t, n) => t
                        ..addAll({
                          n: res?.namedGroup(n),
                          if (remark != null) ...{"remark": remark}
                        })) ??
                  {}
            });

            decoded = true;
            break;
          }
        }

        if (!decoded) {
          if (e.isNotEmpty && e != "X") {
            log("__unknown: $e");
            dayduty.add({
              "type": "unknown",
              "data": {"unknown": e.trim()}
            });
          }
        }
      }
      roster[_date2ymd(linedate)] = dayduty;
      //log("$dateblock ${_date2ymd(linedate)} $dayduty");
    }

    return {
      ...{"info": info},
      ...{"roster": roster}
    };
    // } catch (e) {
    //   log("crewlinkapi: decoderoster: ${e.toString()}");
    //   return {};
    // }
  }

  DateTime _ddmmmyy2date(String date) {
    return DateFormat("ddMMMyy").parseUTC(date);
  }

  String _date2ymd(DateTime date) {
    return DateFormat("y-MM-dd").format(date);
  }
}
