// 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');

const {
  GITKRAKEN_ASKPASS_SESSION_ID,
  GITKRAKEN_ASKPASS_SESSION_ID_SIGN,
  GITKRAKEN_SOCKET_SERVICE_PORT,
  GITKRAKEN_ASKPASS_SSH_KEY,
  GITKRAKEN_ASKPASS_SSH_KEY_SIGN,
  GIT_EXECUTOR_COMMAND_ID
} = process.env;
// "||" to ignore empty string
const GITKRAKEN_GPG_PROGRAM = process.env.GITKRAKEN_GPG_PROGRAM || 'gpg';
debugLog([
  `GITKRAKEN_ASKPASS_SESSION_ID=${GITKRAKEN_ASKPASS_SESSION_ID}`,
  `GITKRAKEN_ASKPASS_SESSION_ID_SIGN=${GITKRAKEN_ASKPASS_SESSION_ID_SIGN}`,
  `GITKRAKEN_SOCKET_SERVICE_PORT=${GITKRAKEN_SOCKET_SERVICE_PORT}`,
  `GITKRAKEN_GPG_PROGRAM=${GITKRAKEN_GPG_PROGRAM}`,
  `GITKRAKEN_ASKPASS_SSH_KEY=${GITKRAKEN_ASKPASS_SSH_KEY}`,
  `GITKRAKEN_ASKPASS_SSH_KEY_SIGN=${GITKRAKEN_ASKPASS_SSH_KEY_SIGN}`,
  `GIT_EXECUTOR_COMMAND_ID=${GIT_EXECUTOR_COMMAND_ID}`
].join(', '));

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

const askPassPromptTypes = {
  USERNAME: 'USERNAME', // username prompt for https credentials
  PASSWORD: 'PASSWORD', // password prompt for https credentials
  PASSPHRASE: 'PASSPHRASE', // prompt for SSH credentials
  PASSPHRASE_SSH_SIGN: 'PASSPHRASE_SSH_SIGN', // prompt for SSH sigining
  UNKNOWN: 'UNKNOWN'
};

const getAskPassPromptTypeAndMatch = () => {
  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); // SSH credentials prompt
  const passphraseMatchSshCommitSign = /^Enter passphrase/g.exec(promptArg); // SSH commit signing prompt

  let promptType;
  let match;
  if (passphraseMatch) {
    promptType = askPassPromptTypes.PASSPHRASE;
    if (GITKRAKEN_ASKPASS_SSH_KEY) {
      match = `${GITKRAKEN_ASKPASS_SSH_KEY}`;
    } else {
      match = passphraseMatch[1];
    }
  } else if (passphraseMatchSshCommitSign && GITKRAKEN_ASKPASS_SSH_KEY_SIGN) {
    promptType = askPassPromptTypes.PASSPHRASE_SSH_SIGN;
    // we are not getting the user signing key in the prompt, hence we get it
    // via environment variable set in GITKRAKEN Client.
    match = `${GITKRAKEN_ASKPASS_SSH_KEY_SIGN}`;
  } else if (usernameMatch) {
    promptType = askPassPromptTypes.USERNAME;
    match = usernameMatch[1];
  } else if (passwordMatch) {
    promptType = askPassPromptTypes.PASSWORD;
    match = passwordMatch[1];
  } else {
    promptType = askPassPromptTypes.UNKNOWN;
    match = '';
  }

  return {
    promptType,
    match: match.trim()
  };
};

const isAskPassPrompt = (promptType) => {
  return (
    promptType === askPassPromptTypes.PASSPHRASE ||
    promptType === askPassPromptTypes.PASSPHRASE_SSH_SIGN ||
    promptType === askPassPromptTypes.USERNAME ||
    promptType === askPassPromptTypes.PASSWORD
  );
};

const askPass = (promptTypeAndMatch) => {
  const {
    promptType,
    match
  } = promptTypeAndMatch;

  const request = {
    s: promptType === askPassPromptTypes.PASSPHRASE_SSH_SIGN
      ? GITKRAKEN_ASKPASS_SESSION_ID_SIGN
      : GITKRAKEN_ASKPASS_SESSION_ID,
    p: parseInt(ppid, 10),
    f: null, // request field
    u: null, // url for request
    g: null // git commandId
  };

  if (GIT_EXECUTOR_COMMAND_ID) {
    request.g = GIT_EXECUTOR_COMMAND_ID;
  }

  if (promptType === askPassPromptTypes.PASSPHRASE || promptType === askPassPromptTypes.PASSPHRASE_SSH_SIGN) {
    request.f = 'p';
  } else if (promptType === askPassPromptTypes.USERNAME) {
    request.f = 'u';
  } else if (promptType === askPassPromptTypes.PASSWORD) {
    request.f = 'pw';
  }

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

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

  // Handle single/double quoted URLs
  const urlMatch = /[a-zA-Z]+:\/\/[^\'|\"]+/.exec(url);
  if (urlMatch && urlMatch?.[0]) {
    request.u = urlMatch[0];
  } else {
    request.u = url;
  }

  if (debugLogEnabled) {
    let requestStr;
    let err;
    try {
      requestStr = JSON.stringify(request);
    } catch (_err) {
      err = _err;
    }

    if (err) {
      debugLog(`Failed to stringify request: ${err?.message}`);
    } else {
      debugLog(`request: ${requestStr}`);
    }
  }

  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_SIGN,
      p: parseInt(ppid, 10),
      f: 'p', // request field
      u: '', // url for request
      g: null // git commandId
    };

    if (GIT_EXECUTOR_COMMAND_ID) {
      request.g = GIT_EXECUTOR_COMMAND_ID;
    }

    // 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: Number.parseInt(GITKRAKEN_SOCKET_SERVICE_PORT ?? '', 10),
      host: '127.0.0.1'
    },
    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`);
    }
  );
};

const promptTypeAndMatch = getAskPassPromptTypeAndMatch();

if (isAskPassPrompt(promptTypeAndMatch.promptType)) {
  askPass(promptTypeAndMatch);
} else {
  gpgProxy()
    .then(() => {
      process.exit(0);
    })
    .catch(() => {
      process.exit(1);
    });
}
