/**
 * Function/Network/public-ip.js
 * ---------------------------------------------------------------------------
 * PUBLIC IP INFORMATION
 *
 * Asks a reputable public service for the address this machine appears as on
 * the internet. The result is only kept in memory - it is written to disk only
 * when the user explicitly exports the report.
 * ---------------------------------------------------------------------------
 */

'use strict';

const https = require('https');
const kit = require('../../core/kit');

/** Services are tried in order until one answers. */
const SERVICES = [
  { name: 'ipify', url: 'https://api.ipify.org?format=json', field: 'ip' },
  { name: 'ifconfig.co', url: 'https://ifconfig.co/json', field: 'ip' },
  { name: 'ipinfo.io', url: 'https://ipinfo.io/json', field: 'ip' },
];

/** Optional detail lookup (city, ISP). Only used when the user asks for it. */
const DETAIL_SERVICE = { name: 'ipinfo.io', url: 'https://ipinfo.io/json' };

function fetchJson(url, timeoutMs = 8000) {
  return new Promise((resolve) => {
    let settled = false;
    const finish = (value) => {
      if (!settled) {
        settled = true;
        resolve(value);
      }
    };

    const request = https.get(
      url,
      { headers: { 'User-Agent': 'InfinitySniper-HFX', Accept: 'application/json' }, timeout: timeoutMs },
      (response) => {
        if (response.statusCode !== 200) {
          response.resume();
          return finish({ ok: false, error: `HTTP ${response.statusCode}` });
        }
        let body = '';
        response.setEncoding('utf8');
        response.on('data', (chunk) => {
          body += chunk;
          if (body.length > 64 * 1024) request.destroy();
        });
        response.on('end', () => {
          try {
            finish({ ok: true, data: JSON.parse(body) });
          } catch {
            finish({ ok: true, data: { ip: body.trim() } });
          }
        });
        return undefined;
      }
    );

    request.on('timeout', () => {
      request.destroy();
      finish({ ok: false, error: 'timeout' });
    });
    request.on('error', (err) => finish({ ok: false, error: err.code || err.message }));
  });
}

async function resolvePublicIp() {
  for (const service of SERVICES) {
    // eslint-disable-next-line no-await-in-loop
    const result = await fetchJson(service.url);
    if (result.ok && result.data && result.data[service.field]) {
      return { ok: true, ip: String(result.data[service.field]).trim(), service: service.name, raw: result.data };
    }
  }
  return { ok: false, error: 'no service answered' };
}

module.exports = {
  name: 'Public IP Information',
  menuName: 'Public IP Information',
  description: 'Shows the public IP address this machine uses, with optional network details.',
  version: '1.0.0',
  author: 'InfinitySniper HFX',
  category: 'Network',
  order: 50,
  requiredPermissions: 'normal',
  destructive: false,
  dependencies: [],
  minimumAppVersion: '1.0.0',
  permissions: ['network:outbound(https)'],
  tags: ['ip', 'public', 'internet', 'wan', 'address'],
  documentation:
    'Contacts a public IP echo service over HTTPS and reports the address your connection presents ' +
    'to the internet. Optional details (city, region, country, provider) come from ipinfo.io. ' +
    'This is the only function that leaves the machine, and it sends nothing but the request ' +
    'itself. The address is not written to disk unless you export the report.',

  async selfTest() {
    // A self-test must not depend on internet access being available.
    return { ok: true, message: 'ready (requires internet at run time)' };
  },

  async execute(ctx) {
    const { ui, prompt } = ctx;
    kit.header('Public IP Information', [
      'Asks a public service how this machine appears online.',
      'The result is not stored unless you export it.',
    ]);

    const spin = ui.spinner('Contacting the IP service...');
    const result = await resolvePublicIp();
    spin.stop();

    if (!result.ok) {
      ui.blank();
      ui.warning('The public IP could not be determined.');
      ui.println('  ' + ui.c.gray(`Reason: ${result.error}`));
      ui.println('  ' + ui.c.gray('Check your internet connection, proxy or firewall and try again.'));
      return { ok: false, reason: 'offline' };
    }

    ui.blank();
    ui.section('PUBLIC IP');
    ui.blank();
    kit.kv('Public IP Address', result.ip, 'brightGreen');
    kit.kv('Service Used', result.service);
    kit.kv('Address Family', result.ip.includes(':') ? 'IPv6' : 'IPv4');

    let details = null;
    ui.blank();
    const wantDetails = await prompt.confirm('Look up the location and provider of this address?');
    if (wantDetails === true) {
      const spin2 = ui.spinner('Requesting details...');
      const response = await fetchJson(DETAIL_SERVICE.url);
      spin2.stop();

      if (response.ok && response.data) {
        details = response.data;
        ui.blank();
        ui.section('NETWORK DETAILS');
        ui.blank();
        kit.kv('Host Name', details.hostname);
        kit.kv('City', details.city);
        kit.kv('Region', details.region);
        kit.kv('Country', details.country);
        kit.kv('Provider (ASN)', details.org);
        kit.kv('Time Zone', details.timezone);
        kit.kv('Postal Code', details.postal);
      } else {
        ui.warning('The detail service did not answer - the IP address above is still valid.');
      }
    }

    ui.blank();
    ui.println('  ' + ui.c.gray('Your public IP identifies your connection - be careful where you share it.'));

    await kit.offerExport('Public IP Report', 'PublicIP', (report) => {
      const section = report.section('PUBLIC IP');
      section.add('Public IP Address', result.ip);
      section.add('Service Used', result.service);
      section.add('Address Family', result.ip.includes(':') ? 'IPv6' : 'IPv4');
      if (details) {
        const group = section.group('Network Details');
        group.add('Host Name', details.hostname);
        group.add('City', details.city);
        group.add('Region', details.region);
        group.add('Country', details.country);
        group.add('Provider', details.org);
        group.add('Time Zone', details.timezone);
      }
      report.footerNote('This report contains your public IP address - treat it as personal data.');
    });

    return kit.done('Public IP lookup finished.');
  },
};
