import parse from 'csv-parse';
import util from 'util';
import fs from 'fs';
import path from 'path';
import YAML from 'yaml';
import _ from 'lodash';
import child_process from 'child_process';
import assert from 'assert';

class InventoryBuilder {
  hosts: { [key: string]: any };
  gateways: any;
  connections: string[];
  route_lists: any;

  async load(sheetName) {
    const data = await fs.promises.readFile(path.join('data', `内网互联计划 - ${sheetName}.csv`));
    // @ts-ignore
    return (await util.promisify(parse)(data, { columns: true, cast: true })).filter(h => h.id);
  }

  async loadGateways() {
    let gateways = await this.load('gateways');
    for (let gateway of gateways) {
      if (!gateway.dev_or_via.length)
        continue;
      gateway.isTun = !gateway.dev_or_via.match(/^(\d{1,3}\.){1,3}\d{1,3}$/);
    }
    return gateways;
  }

  async main() {
    this.hosts = _.keyBy(await this.load('nextgen links'), 'name');
    this.gateways = _.mapValues(_.groupBy(await this.loadGateways(), 'name'), g => _.keyBy(g, 'isp'));
    //console.log(this.gateways);
    this.connections = _.intersection(Object.keys(this.hosts), Object.keys(_.find(this.hosts)));

    for (const host of Object.values(this.hosts)) {
      host.wgPublickey = await this.wgPublickey(host.wgPrivateKey);
    }
    // console.log(Object.values(this.hosts));
    const hosts = Object.fromEntries(Object.values(this.hosts).map(h => [h.host, this.host_vars(h)]));
    // console.log(hosts);
    const vars = await this.loadUtilities(hosts);
    const result = YAML.stringify({ wg: { hosts, vars } });
    return fs.promises.writeFile('result/inventory.yaml', result);
  }

  async loadUtilities(hosts) {
    const raw_utility = _.keyBy(await this.load('configurations'), 'key');
    let route_plans = await this.load('route tables');
    this.route_lists = YAML.parse(fs.readFileSync(path.join('lists', 'result.yaml'), "utf8"));
    for (let plan of route_plans) {
      plan.name = plan.list + "_" + plan.gateway.replace(/-/g, "_").split(".")[0]
      //plan.list = JSON.parse(JSON.stringify(this.route_lists[plan.list]));
      plan.gatewayAddress = hosts[plan.gateway].address;
    }
    // 所有内网网段
    this.route_lists.mycard = ["10.198.0.0/16", "10.200.0.0/15"];
    for (const h in this.hosts) {
      const host = this.hosts[h]
      for (const c of host.subnets.split(",")) {
        if (!c.length) {
          continue;
        }
        this.route_lists.mycard.push(c);
      }
    }
    // temp user before gateways
    this.route_lists.ladder_needed = raw_utility.ladderNeeded.value.split(",").map((m) => { return m.trim() });
    const vars = {
      route_lists: this.route_lists,
      route_list_names: Object.keys(this.route_lists),
      route_plans
    };
    for (let col in raw_utility) {
      vars[col] = raw_utility[col].value;
    }

    return vars;
  }
  host_vars(host) {
    const connections = [];
    const null_connection = "10000,null";
    const lan_interfaces = host.lan_interfaces.length > 0 ? host.lan_interfaces.split(",") : [];
    const dns = host.dns;
    for (const h of this.connections) {
      if (h != host.name) {
        const to = host[h];
        const from = this.hosts[h][host.name];
        if (from && to) {
          // 非对称连接
          connections.push(this.parse_connection(host, this.hosts[h], to, false, true));
          connections.push(this.parse_connection(host, this.hosts[h], from, true, false));
        } else if (from || to) {
          // 对称连接
          connections.push(this.parse_connection(host, this.hosts[h], from || to, true, true));
          connections.push(this.parse_connection(host, this.hosts[h], null_connection, false, false));
        } else {
          // 不连接
          connections.push(this.parse_connection(host, this.hosts[h], null_connection, true, false));
          connections.push(this.parse_connection(host, this.hosts[h], null_connection, false, true));
        }
      }
    }

    return {
      ansible_ssh_user: host.user,
      address: host.address,
      isCN: host.location.startsWith("CN"),
      key: host.wgPrivateKey,
      frpsPort: host.frpsPort,
      chinaMirror: host.chinaMirror,
      //frpToken: host.frpToken,
      //gateways: _.mapValues(this.gateways[host.name], gw => _.pick(gw, ['mark_gateway'])),
      gateways: _.values(this.gateways[host.name]),
      connections,
      lan_interfaces,
      dns
    };
  }

  parse_connection(local: any, remote: any, connstr: string, inbound: boolean, outbound: boolean) {
    const leftbottom = local.id > remote.id; // true 条目位于左下，false 条目位于右上
    const cis = !(!leftbottom && inbound && outbound); // true 无需翻转，false 需要翻转。
    const primary = leftbottom ? outbound : inbound; // true 使用 peerAddress、port, false 使用peerAddress2、port2

    const [_metric, protocol, _params] = connstr.split(',');
    const metric = parseInt(_metric);
    const params = Object.fromEntries(new URLSearchParams(_params).entries());
    const name = `mc${!outbound ? 'i' : '-'}${remote.name}`;
    const localGatewayName = (cis ? params.lif : params.rif) || params.if;
    const localGateway = localGatewayName ? this.gateways[local.name][localGatewayName] : _.find(this.gateways[local.name]);
    const localGatewayMark = localGatewayName ? localGateway.mark : "";
    const remoteGatewayName = (cis ? params.rif : params.lif) || params.if;
    const remoteGateway = remoteGatewayName ? this.gateways[remote.name][remoteGatewayName] : _.find(this.gateways[remote.name]);
    //const remoteGatewayMark = remoteGatewayName ? remoteGateway.mark : undefined;
    //console.log(remoteGateway.name);
    const remoteAddress = remoteGateway.address;
    const remoteLocalAddress = remote.address;
    const remoteMark = remote.mark;
    const localPort = (primary ? remote.port : remote.port2) + local.offset;
    const remotePort = (primary ? local.port : local.port2) + remote.offset;
    const remoteFrpsPort = remote.frpsPort;
    const wgPublicKey = remote.wgPublickey;
    const localPeerAddress = primary ? `10.200.${local.id}.${remote.id}` : `10.201.${local.id}.${remote.id}`;
    const remotePeerAddress = primary ? `10.200.${remote.id}.${local.id}` : `10.201.${remote.id}.${local.id}`;

    const frpType = protocol === 'wgfrp' ? (this.gatewayCompare(localGateway, remoteGateway) ? 'frps' : 'frpc') : undefined;

    const mtu = Math.min(localGateway ? localGateway.mtu : 1500, remoteGateway ? remoteGateway.mtu : 1500);
    //console.log(local.name, name, mtu);

    return {
      name,
      metric,
      protocol,
      params,
      localGatewayMark,
      //remoteGatewayMark,
      remoteMark,
      remoteAddress,
      remoteLocalAddress,
      localPort,
      remotePort,
      wgPublicKey,
      localPeerAddress,
      remotePeerAddress,
      remoteFrpsPort,
      frpType,
      inbound,
      outbound,
      mtu
    };
  }

  // frps还是frpc的积分，NAT越有利分越高
  gatewayCompareScore(gateway: any): number { 
    let score: number = 0xff - gateway.id;
    const ipv4Score = ({
      "static": 2,
      "dynamic": 1
    })[gateway.ipv4] || 0;
    score |= ipv4Score << 12;
    const ipv4NatScore = ({
      "ports": 0,
      "dmz": 1
    })[gateway.ipv4_nat] || 2;
    score |= ipv4NatScore << 10;
    const globalSSHScore = ({
      "globalssh": 1
    })[gateway.ssh] || 0;
    score |= globalSSHScore << 13;
    return score;
  }

  // true: 本地做 server，false: 远端做server
  // 如果都不能做，抛异常
  // 两个参数对调返回的结果必须相反
  gatewayCompare(localGateway: any, remoteGateway: any): boolean {
    // 两边至少一个有IPv4地址才能连
    assert(localGateway.ipv4 !== '' || remoteGateway.ipv4 !== '');
    // 两边必须相反
    assert(this.gatewayCompareScore(localGateway) !== this.gatewayCompareScore(remoteGateway));
    // 两边只有一边是GlobalSSH的，GlobalSSH做s
    // 只有一边有static的，就static做s
    // 如果都static，那么没有NAT的或者dmz的做s
    // 如果都还相同，就让列表中更靠前的做s
    return this.gatewayCompareScore(localGateway) > this.gatewayCompareScore(remoteGateway);
  }

  async wgPublickey(privateKey) {
    return new Promise((resolve, reject) => {
      const child = child_process.execFile('wg', ['pubkey'], { encoding: 'utf8' }, (error, stdout, stderr) => {
        if (stderr) {
          console.warn(stderr);
        }
        if (error) {
          reject(error);
        } else {
          resolve(stdout.trimEnd());
        }
      });
      child.stdin.end(privateKey);
    });
  }
}

new InventoryBuilder().main();

// export interface Host {
//
// }

// export interface Interface {
//
// }
