// NOTE: This file can only require from the standard library, as it is a
// standalone script and not bundled.

// Before enabling, keep in mind that some processes (e.g. git when spawning gpg)
// process stderr as well as stdout, so logging to stderr can cause failures
// Can pass 1/true/stderr to log to stderr or a path to log to a file
const debugLogEnabled = process.env.GITKRAKEN_DEBUG_ASKPASS;
let fs;
const debugLog = (message) => {
  if (!debugLogEnabled) {
    return;
  }

  try {
    const timestamp = (new Date()).toISOString();
    message = `[${process.pid}] [${timestamp}]: ${message}\n`;
    if (['1', 'true', 'stderr'].includes(debugLogEnabled)) {
      process.stderr.write(message);
      return;
    }

    if (!fs) {
      fs = require('fs');
    }

    fs.appendFileSync(debugLogEnabled, message);
  } catch {
    // ignore
  }
};

debugLog(`IN ASKPASS, args: ${process.argv.join(',')}`);

const cp = require('child_process');
const net = require('net');

// Make sure to keep in sync with `AskPassConstants.js`
const askPassFunctions = {
  GIT_ASKPASS: 'gitAskpass',
  GPG_PROXY: 'gpgProxy'
};

const {
  GITKRAKEN_ASKPASS_FUNCTION = askPassFunctions.GIT_ASKPASS,
  GITKRAKEN_ASKPASS_SESSION_ID,
  GITKRAKEN_SOCKET_SERVICE_PORT
} = process.env;
// "||" to ignore empty string
const GITKRAKEN_GPG_PROGRAM = process.env.GITKRAKEN_GPG_PROGRAM || 'gpg';
debugLog(`GITKRAKEN_ASKPASS_FUNCTION=${
  GITKRAKEN_ASKPASS_FUNCTION
}, GITKRAKEN_ASKPASS_SESSION_ID=${
  GITKRAKEN_ASKPASS_SESSION_ID
}, GITKRAKEN_SOCKET_SERVICE_PORT=${
  GITKRAKEN_SOCKET_SERVICE_PORT
}, GITKRAKEN_GPG_PROGRAM=${
  GITKRAKEN_GPG_PROGRAM
}`);

const [, , ppid, ...args] = process.argv;

const askPass = () => {
  const request = {
    s: GITKRAKEN_ASKPASS_SESSION_ID,
    p: parseInt(ppid, 10),
    t: GITKRAKEN_ASKPASS_FUNCTION,
    f: null, // request field
    u: null // url for request
  };

  const promptArg = args.join(' ');
  const usernameMatch = /^Username for (.+)/g.exec(promptArg);
  const passwordMatch = /^Password for (.+)/g.exec(promptArg);
  const passphraseMatch = /^Enter passphrase for key (.+)/g.exec(promptArg);

  let match;
  if (usernameMatch) {
    request.f = 'u';
    match = usernameMatch;
  } else if (passwordMatch) {
    request.f = 'pw';
    match = passwordMatch;
  } else if (passphraseMatch) {
    request.f = 'p';
    match = passphraseMatch;
  }

  if (!request.f) {
    process.exit(1);
  }

  let url = match[1].trim();
  if (url.endsWith(':')) {
    url = url.substr(0, url.length - 1);
  }

  // Handle single quoted URLs (SSH)
  const urlMatch = /^'(.+)'$/.exec(url);
  if (urlMatch?.[1]) {
    request.u = urlMatch[1];
  } else {
    request.u = url;
  }

  setupClient((data) => {
    const {
      r: result,
      c: exitCode
    } = JSON.parse(data.toString());
    if (result) {
      process.stdout.write(result);
      process.exit(0);
    } else if (exitCode) {
      process.exit(exitCode);
    } else {
      process.exit(1);
    }
  }, JSON.stringify(request));
};

const runGpgProcess = passphrase => new Promise((resolve, reject) => {
  const gpgProgram = GITKRAKEN_GPG_PROGRAM;
  const gpgArgs = [
    '--batch',
    '--no-tty',
    '--passphrase-fd=0',
    '--pinentry-mode=loopback',
    ...args
  ];

  debugLog(`gpgProgram: ${gpgProgram}, gpgArgs: ${gpgArgs.join(',')}`);
  let handle;
  try {
    handle = cp.spawn(gpgProgram, gpgArgs);
    debugLog('spawn success');
  } catch (err) {
    debugLog(`spawn error: ${err.message}`);
    throw err;
  }

  handle.on('error', (...errorArgs) => {
    debugLog(`errorArgs: ${errorArgs.join(',')}`);
  });

  handle.stdin.write(`${passphrase ?? ''}\n`);
  debugLog(`wrote passphrase: ${passphrase}`);

  process.stdin.on('data', (data) => {
    debugLog(`got process stdin: ${data.toString()}`);
    handle.stdin.write(data);
  });

  process.stdin.on('close', () => {
    debugLog('stdin closed');
    handle.stdin.end();
  });

  handle.stdout.on('data', (data) => {
    debugLog(`process stdout: ${data}`);
    process.stdout.write(data);
  });

  handle.stderr.on('data', (data) => {
    debugLog(`process stderr: ${data}`);
    process.stderr.write(data);
  });

  handle.on('close', (code) => {
    debugLog(`closed: ${code}`);
    if (code === 0) {
      resolve(code);
    } else {
      reject(code);
    }
  });
});

const gpgProxy = () => new Promise((resolve, reject) => {
  const request = {
    s: GITKRAKEN_ASKPASS_SESSION_ID,
    p: parseInt(ppid, 10),
    t: GITKRAKEN_ASKPASS_FUNCTION,
    f: 'p', // request field
    u: '' // url for request
  };

  // TODO(git-cli) ensure GPG only executted once
  setupClient((data) => {
    const {
      r: result,
      c: exitCode
    } = JSON.parse(data.toString());
    if (exitCode) {
      debugLog(`exitCode: ${exitCode}`);
      process.exit(exitCode);
    }

    runGpgProcess(result).then(resolve).catch(reject);
  }, JSON.stringify(request), false);
});

const performHandshake = clientSocket => new Promise((resolve, reject) => {
  clientSocket.on('error', reject);
  clientSocket.on('close', reject);
  clientSocket.on('end', reject);

  debugLog('waiting for handshake');
  clientSocket.once('data', (data) => {
    debugLog(`handshake data: ${data.toString()}`);
    clientSocket.removeListener('error', reject);
    clientSocket.removeListener('close', reject);
    clientSocket.removeListener('end', reject);

    if (data.toString() !== 'ready') {
      reject();
    } else {
      resolve();
    }
  });

  clientSocket.write('ask_pass');
});

// a NUL byte (\0) will be appended to dataToWrite
const setupClient = (onData, dataToWrite, exitOnEnd = true) => {
  const client = net.createConnection(
    {
      port: GITKRAKEN_SOCKET_SERVICE_PORT,
      host: 'localhost'
    },
    async () => {
      try {
        await performHandshake(client);
      } catch (error) {
        debugLog(`handshake error: ${error.message}`);
        process.exit(1);
      }

      client.on('error', () => {
        debugLog('client error');
        process.exit(1);
      });
      client.on('end', () => {
        debugLog('client end');
        if (exitOnEnd) {
          process.exit(1);
        }
      });

      client.on('data', (data) => {
        try {
          debugLog(`client data(${data.length}): ${data.toString()}`);
          onData(data);
        } catch (error) {
          debugLog(`client data error: ${error.message}`);
          process.exit(1);
        }
      });
      client.write(`${dataToWrite}\0`);
      debugLog(`wrote data: ${dataToWrite} + NUL`);
    }
  );
};

switch (GITKRAKEN_ASKPASS_FUNCTION) {
  case askPassFunctions.GIT_ASKPASS:
    askPass();
    break;

  case askPassFunctions.GPG_PROXY:
    gpgProxy().then(() => {
      process.exit(0);
    }).catch(() => {
      process.exit(1);
    });
    break;

  default:
    process.exit(1);
}
