/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import * as fs from 'fs';
// @ts-ignore - clack is ESM and TS complains about that. It works though
import clack from '@clack/prompts';
import chalk from 'chalk';

type BuildPhase = { shellScript: string };
type BuildPhaseMap = Record<string, BuildPhase>;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getValidExistingBuildPhases(xcodeProject: any): BuildPhaseMap {
  const map: BuildPhaseMap = {};
  const raw = xcodeProject.hash.project.objects.PBXShellScriptBuildPhase || {};
  for (const key in raw) {
    const val = raw[key];
    val.isa && (map[key] = val);
  }

  return map;
}

export function patchBundlePhase(
  bundlePhase: BuildPhase | undefined,
  patch: (script: string) => string,
) {
  if (!bundlePhase) {
    clack.log.warn(
      `Could not find ${chalk.cyan(
        'Bundle React Native code and images',
      )} build phase.`,
    );
    return;
  }

  const bundlePhaseIncludesSentry = doesBundlePhaseIncludeSentry(bundlePhase);
  if (bundlePhaseIncludesSentry) {
    clack.log.warn(
      `Build phase ${chalk.cyan(
        'Bundle React Native code and images',
      )} already includes Sentry.`,
    );
    return;
  }

  const script: string = JSON.parse(bundlePhase.shellScript);
  bundlePhase.shellScript = JSON.stringify(patch(script));
  clack.log.success(
    `Patched Build phase ${chalk.cyan('Bundle React Native code and images')}.`,
  );
}

export function unPatchBundlePhase(bundlePhase: BuildPhase | undefined) {
  if (!bundlePhase) {
    clack.log.warn(
      `Could not find ${chalk.cyan(
        'Bundle React Native code and images',
      )} build phase.`,
    );
    return;
  }

  if (
    !bundlePhase.shellScript.match(/sentry-cli\s+react-native\s+xcode/i) &&
    !bundlePhase.shellScript.includes('sentry-xcode.sh')
  ) {
    clack.log.success(
      `Build phase ${chalk.cyan(
        'Bundle React Native code and images',
      )} does not include Sentry.`,
    );
    return;
  }

  bundlePhase.shellScript = JSON.stringify(
    removeSentryFromBundleShellScript(
      <string>JSON.parse(bundlePhase.shellScript),
    ),
  );
  clack.log.success(
    `Build phase ${chalk.cyan(
      'Bundle React Native code and images',
    )} unpatched successfully.`,
  );
}

export function removeSentryFromBundleShellScript(script: string): string {
  return (
    script
      // remove sentry properties export
      .replace(/^export SENTRY_PROPERTIES=sentry.properties\r?\n/m, '')
      .replace(
        /^\/bin\/sh .*?..\/node_modules\/@sentry\/react-native\/scripts\/collect-modules.sh"?\r?\n/m,
        '',
      )
      // unwrap react-native-xcode.sh command.  In case someone replaced it
      // entirely with the sentry-cli command we need to put the original
      // version back in.
      .replace(
        /\.\.\/node_modules\/@sentry\/cli\/bin\/sentry-cli\s+react-native\s+xcode\s+\$REACT_NATIVE_XCODE/i,
        '$REACT_NATIVE_XCODE',
      )
      .replace(
        //  eslint-disable-next-line no-useless-escape
        /\"\/bin\/sh.*?sentry-xcode.sh\s+\$REACT_NATIVE_XCODE/i,
        // eslint-disable-next-line no-useless-escape
        '"$REACT_NATIVE_XCODE',
      )
  );
}

export function findBundlePhase(buildPhases: BuildPhaseMap) {
  return Object.values(buildPhases).find((buildPhase) =>
    buildPhase.shellScript.match(/\/scripts\/react-native-xcode\.sh/i),
  );
}

export function doesBundlePhaseIncludeSentry(buildPhase: BuildPhase) {
  const containsSentryCliRNCommand = !!buildPhase.shellScript.match(
    /sentry-cli\s+react-native\s+xcode/i,
  );
  const containsBundledScript =
    buildPhase.shellScript.includes('sentry-xcode.sh');
  return containsSentryCliRNCommand || containsBundledScript;
}

export function addSentryWithBundledScriptsToBundleShellScript(
  script: string,
): string {
  const isLikelyPlainReactNativeScript = script.includes('$REACT_NATIVE_XCODE');
  if (isLikelyPlainReactNativeScript) {
    return script.replace(
      '$REACT_NATIVE_XCODE',
      // eslint-disable-next-line no-useless-escape
      '\\"/bin/sh ../node_modules/@sentry/react-native/scripts/sentry-xcode.sh $REACT_NATIVE_XCODE\\"',
    );
  }

  const isLikelyExpoScript = script.includes('expo');
  if (isLikelyExpoScript) {
    const SENTRY_REACT_NATIVE_XCODE_PATH =
      "`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode.sh'\"`";
    return script.replace(
      /^.*?(packager|scripts)\/react-native-xcode\.sh\s*(\\'\\\\")?/m,
      // eslint-disable-next-line no-useless-escape
      (match: string) => `/bin/sh ${SENTRY_REACT_NATIVE_XCODE_PATH} ${match}`,
    );
  }

  return script;
}

export function addSentryWithCliToBundleShellScript(script: string): string {
  return (
    'export SENTRY_PROPERTIES=sentry.properties\n' +
    'export EXTRA_PACKAGER_ARGS="--sourcemap-output $DERIVED_FILE_DIR/main.jsbundle.map"\n' +
    script.replace(
      '$REACT_NATIVE_XCODE',
      () =>
        // eslint-disable-next-line no-useless-escape
        '\\"../node_modules/@sentry/cli/bin/sentry-cli react-native xcode $REACT_NATIVE_XCODE\\"',
    ) +
    '\n/bin/sh -c "$WITH_ENVIRONMENT ../node_modules/@sentry/react-native/scripts/collect-modules.sh"\n'
  );
}

export function addDebugFilesUploadPhaseWithBundledScripts(
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  xcodeProject: any,
  { debugFilesUploadPhaseExists }: { debugFilesUploadPhaseExists: boolean },
) {
  if (debugFilesUploadPhaseExists) {
    clack.log.warn(
      `Build phase ${chalk.cyan(
        'Upload Debug Symbols to Sentry',
      )} already exists.`,
    );
    return;
  }

  xcodeProject.addBuildPhase(
    [],
    'PBXShellScriptBuildPhase',
    'Upload Debug Symbols to Sentry',
    null,
    {
      shellPath: '/bin/sh',
      shellScript: `/bin/sh ../node_modules/@sentry/react-native/scripts/sentry-xcode-debug-files.sh`,
    },
  );
  clack.log.success(
    `Added Build phase ${chalk.cyan('Upload Debug Symbols to Sentry')}.`,
  );
}

export function addDebugFilesUploadPhaseWithCli(
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  xcodeProject: any,
  { debugFilesUploadPhaseExists }: { debugFilesUploadPhaseExists: boolean },
) {
  if (debugFilesUploadPhaseExists) {
    clack.log.warn(
      `Build phase ${chalk.cyan(
        'Upload Debug Symbols to Sentry',
      )} already exists.`,
    );
    return;
  }

  xcodeProject.addBuildPhase(
    [],
    'PBXShellScriptBuildPhase',
    'Upload Debug Symbols to Sentry',
    null,
    {
      shellPath: '/bin/sh',
      shellScript: `
WITH_ENVIRONMENT="../node_modules/react-native/scripts/xcode/with-environment.sh"
if [ -f "$WITH_ENVIRONMENT" ]; then
  . "$WITH_ENVIRONMENT"
fi
export SENTRY_PROPERTIES=sentry.properties
[ "$SENTRY_INCLUDE_NATIVE_SOURCES" = "true" ] && INCLUDE_SOURCES_FLAG="--include-sources" || INCLUDE_SOURCES_FLAG=""
../node_modules/@sentry/cli/bin/sentry-cli debug-files upload "$INCLUDE_SOURCES_FLAG" "$DWARF_DSYM_FOLDER_PATH"
`,
    },
  );
  clack.log.success(
    `Added Build phase ${chalk.cyan('Upload Debug Symbols to Sentry')}.`,
  );
}

export function unPatchDebugFilesUploadPhase(
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  xcodeProject: any,
) {
  const buildPhasesMap =
    xcodeProject.hash.project.objects.PBXShellScriptBuildPhase || {};

  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
  const debugFilesUploadPhaseResult = findDebugFilesUploadPhase(buildPhasesMap);
  if (!debugFilesUploadPhaseResult) {
    clack.log.success(
      `Build phase ${chalk.cyan('Upload Debug Symbols to Sentry')} not found.`,
    );
    return;
  }

  const [debugFilesUploadPhaseKey] = debugFilesUploadPhaseResult;
  const firstTarget: string = xcodeProject.getFirstTarget().uuid;
  const nativeTargets = xcodeProject.hash.project.objects.PBXNativeTarget;

  // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
  delete buildPhasesMap[debugFilesUploadPhaseKey];
  // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
  delete buildPhasesMap[`${debugFilesUploadPhaseKey}_comment`];
  const phases = nativeTargets[firstTarget].buildPhases;
  if (phases) {
    for (let i = 0; i < phases.length; i++) {
      if (phases[i].value === debugFilesUploadPhaseKey) {
        phases.splice(i, 1);
        break;
      }
    }
  }
  clack.log.success(
    `Build phase ${chalk.cyan(
      'Upload Debug Symbols to Sentry',
    )} removed successfully.`,
  );
}

export function findDebugFilesUploadPhase(
  buildPhasesMap: Record<string, BuildPhase>,
): [key: string, buildPhase: BuildPhase] | undefined {
  return Object.entries(buildPhasesMap).find(([_, buildPhase]) => {
    const containsCliDebugUpload =
      typeof buildPhase !== 'string' &&
      !!buildPhase.shellScript.match(
        /sentry-cli\s+(upload-dsym|debug-files upload)\b/,
      );
    const containsBundledDebugUpload =
      typeof buildPhase !== 'string' &&
      buildPhase.shellScript.includes('sentry-xcode-debug-files.sh');
    return containsCliDebugUpload || containsBundledDebugUpload;
  });
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function writeXcodeProject(xcodeProjectPath: string, xcodeProject: any) {
  const newContent = xcodeProject.writeSync();
  const currentContent = fs.readFileSync(xcodeProjectPath, 'utf-8');
  if (newContent === currentContent) {
    return;
  }

  fs.writeFileSync(xcodeProjectPath, newContent, 'utf-8');
  clack.log.success(
    chalk.green(`Xcode project ${chalk.cyan(xcodeProjectPath)} changes saved.`),
  );
}
