import * as Expo from "@expo/config-plugins";
import { assignColorValue } from "@expo/config-plugins/build/android/Colors";
import { addImports } from "@expo/config-plugins/build/android/codeMod";
import { mergeContents } from "@expo/config-plugins/build/utils/generateCode";
import childProcess from "child_process";
import path from "path";
import semver from "semver";
import { dedent } from "ts-dedent";
import util from "util";
import {
  type BootSplashPluginConfig,
  hfs,
  log,
  PACKAGE_NAME,
  requireAddon,
  setLoggerMode,
  transformProps,
  writeAndroidAssets,
  writeGenericAssets,
  writeIOSAssets,
  writeWebAssets,
} from "./utils";

const promisifiedExec = util.promisify(childProcess.exec);

const exec = (cmd: string) =>
  promisifiedExec(cmd).then(({ stdout, stderr }) => stdout || stderr);

type ConfigPlugin = Expo.ConfigPlugin<BootSplashPluginConfig>;

const withoutExpoSplashScreen: ConfigPlugin = Expo.createRunOncePlugin(
  (expoConfig) => expoConfig,
  "expo-splash-screen",
  "skip",
);

const withAndroidAssets: ConfigPlugin = (expoConfig, rawProps) =>
  Expo.withDangerousMod(expoConfig, [
    "android",
    async (config) => {
      const { platformProjectRoot, projectRoot } = config.modRequest;
      const props = await transformProps(projectRoot, rawProps);
      const addon = requireAddon(props);

      const androidOutputPath = path.resolve(
        platformProjectRoot,
        "app",
        "src",
        "main",
        "res",
      );

      await writeAndroidAssets({ androidOutputPath, props });
      await addon?.writeAndroidAssets({ androidOutputPath, props });

      return config;
    },
  ]);

const withAndroidManifest: ConfigPlugin = (expoConfig) =>
  Expo.withAndroidManifest(expoConfig, (config) => {
    config.modResults.manifest.application?.forEach((application) => {
      if (application.$["android:name"] === ".MainApplication") {
        const { activity } = application;

        activity?.forEach((activity) => {
          if (activity.$["android:name"] === ".MainActivity") {
            activity.$["android:theme"] = "@style/BootTheme";
          }
        });
      }
    });

    return config;
  });

const withMainActivity: ConfigPlugin = (expoConfig) =>
  Expo.withMainActivity(expoConfig, (config) => {
    const { modResults } = config;

    const withImports = addImports(
      modResults.contents.replace(
        /(\/\/ )?setTheme\(R\.style\.AppTheme\)/,
        "// setTheme(R.style.AppTheme)",
      ),
      ["android.os.Bundle", "com.zoontek.rnbootsplash.RNBootSplash"],
      false,
    );

    // indented with 4 spaces
    const withInit = mergeContents({
      src: withImports,
      comment: "    //",
      tag: "bootsplash-init",
      offset: 0,
      anchor: /super\.onCreate\((null|savedInstanceState)\)/,
      newSrc: "    RNBootSplash.init(this, R.style.BootTheme)",
    });

    return {
      ...config,
      modResults: {
        ...modResults,
        contents: withInit.contents,
      },
    };
  });

const withAndroidStyles: ConfigPlugin = (expoConfig, rawProps) =>
  Expo.withAndroidStyles(expoConfig, async (config) => {
    const { projectRoot } = config.modRequest;
    const { android, brand } = await transformProps(projectRoot, rawProps);
    const { darkContentBarsStyle } = android;

    const { modResults } = config;
    const { resources } = modResults;
    const { style = [] } = resources;

    const item = [
      {
        $: { name: "postBootSplashTheme" },
        _: "@style/AppTheme",
      },
      {
        $: { name: "bootSplashBackground" },
        _: "@color/bootsplash_background",
      },
      {
        $: { name: "bootSplashLogo" },
        _: "@drawable/bootsplash_logo",
      },
    ];

    if (brand != null) {
      item.push({
        $: { name: "bootSplashBrand" },
        _: "@drawable/bootsplash_brand",
      });
    }
    if (darkContentBarsStyle != null) {
      item.push({
        $: { name: "darkContentBarsStyle" },
        _: String(darkContentBarsStyle),
      });
    }

    const withBootTheme = [
      ...style.filter(({ $ }) => $.name !== "BootTheme"),
      {
        $: {
          name: "BootTheme",
          parent: "Theme.BootSplash",
        },
        item,
      },
    ];

    return {
      ...config,
      modResults: {
        ...modResults,
        resources: {
          ...resources,
          style: withBootTheme,
        },
      },
    };
  });

const withAndroidColors: ConfigPlugin = (expoConfig, rawProps) =>
  Expo.withAndroidColors(expoConfig, async (config) => {
    const { projectRoot } = config.modRequest;
    const { background } = await transformProps(projectRoot, rawProps);

    config.modResults = assignColorValue(config.modResults, {
      name: "bootsplash_background",
      value: background.hex,
    });

    return config;
  });

const withAndroidColorsNight: ConfigPlugin = (expoConfig, rawProps) =>
  Expo.withAndroidColorsNight(expoConfig, async (config) => {
    const { projectRoot } = config.modRequest;
    const props = await transformProps(projectRoot, rawProps);
    const addon = requireAddon(props);

    return addon != null
      ? await addon.withAndroidColorsNight({ config, props })
      : config;
  });

const withIOSAssets: ConfigPlugin = (expoConfig, rawProps) =>
  Expo.withDangerousMod(expoConfig, [
    "ios",
    async (config) => {
      const {
        platformProjectRoot,
        projectName = "",
        projectRoot,
      } = config.modRequest;

      const props = await transformProps(projectRoot, rawProps);
      const addon = requireAddon(props);
      const iosOutputPath = path.resolve(platformProjectRoot, projectName);

      await writeIOSAssets({ iosOutputPath, props });
      await addon?.writeIOSAssets({ iosOutputPath, props });

      return config;
    },
  ]);

const withAppDelegate: ConfigPlugin = (expoConfig) =>
  Expo.withAppDelegate(expoConfig, (config) => {
    const { modResults } = config;
    const { language } = modResults;

    if (language !== "swift") {
      throw new Error(
        `Cannot modify the project AppDelegate as it's not in a supported language: ${language}`,
      );
    }

    const withHeader = mergeContents({
      src: modResults.contents,
      comment: "//",
      tag: "bootsplash-header",
      offset: 1,
      anchor: /import Expo/,
      newSrc: "import RNBootSplash",
    });

    const withRootView = mergeContents({
      src: withHeader.contents,
      comment: "//",
      tag: "bootsplash-init",
      offset: 1,
      anchor: /class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {/,
      newSrc: dedent`
        public override func customize(_ rootView: UIView) {
          super.customize(rootView)
          RNBootSplash.initWithStoryboard("BootSplash", rootView: rootView)
        }
      `,
    });

    return {
      ...config,
      modResults: {
        ...modResults,
        contents: withRootView.contents,
      },
    };
  });

const withInfoPlist: ConfigPlugin = (expoConfig) =>
  Expo.withInfoPlist(expoConfig, (config) => {
    config.modResults["UILaunchStoryboardName"] = "BootSplash";
    return config;
  });

const withXcodeProject: ConfigPlugin = (expoConfig) =>
  Expo.withXcodeProject(expoConfig, (config) => {
    const { projectName = "" } = config.modRequest;

    Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
      filepath: path.join(projectName, "BootSplash.storyboard"),
      groupName: projectName,
      project: config.modResults,
      isBuildFile: true,
    });

    Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
      filepath: path.join(projectName, "Colors.xcassets"),
      groupName: projectName,
      project: config.modResults,
      isBuildFile: true,
    });

    return config;
  });

const withWebAssets: ConfigPlugin = (expoConfig, rawProps) =>
  Expo.withDangerousMod(expoConfig, [
    expoConfig.platforms?.includes("ios") ? "ios" : "android",
    async (config) => {
      const { projectRoot } = config.modRequest;
      const props = await transformProps(projectRoot, rawProps);
      const addon = requireAddon(props);

      const fileName = "public/index.html";
      const htmlTemplatePath = path.resolve(projectRoot, fileName);

      if (!hfs.exists(htmlTemplatePath)) {
        await exec(`npx expo customize ${fileName}`);
      }

      await writeWebAssets({ htmlTemplatePath, props });
      await addon?.writeWebAssets({ htmlTemplatePath, props });

      return config;
    },
  ]);

const withGenericAssets: ConfigPlugin = (expoConfig, rawProps) =>
  Expo.withDangerousMod(expoConfig, [
    expoConfig.platforms?.includes("ios") ? "ios" : "android",
    async (config) => {
      const { projectRoot } = config.modRequest;
      const props = await transformProps(projectRoot, rawProps);
      const addon = requireAddon(props);

      await writeGenericAssets({ props });
      await addon?.writeGenericAssets({ props });

      return config;
    },
  ]);

export const withBootSplash = Expo.createRunOncePlugin<
  (Partial<BootSplashPluginConfig> & { logo: string }) | undefined
>((expoConfig, rawProps) => {
  const { platforms = [], sdkVersion = "0.1.0" } = expoConfig;

  setLoggerMode({ type: "plugin" });

  if (semver.lt(sdkVersion, "54.0.0")) {
    log.error("Requires Expo 54.0.0 (or higher)");
    process.exit(1);
  }

  if (!rawProps?.logo) {
    log.error("Missing required parameter 'logo'");
    process.exit(1);
  }

  const plugins: ConfigPlugin[] = [withoutExpoSplashScreen, withGenericAssets];

  if (platforms.includes("android")) {
    plugins.push(
      withAndroidAssets,
      withAndroidManifest,
      withMainActivity,
      withAndroidStyles,
      withAndroidColors,
      withAndroidColorsNight,
    );
  }

  if (platforms.includes("ios")) {
    plugins.push(
      withIOSAssets,
      withAppDelegate,
      withInfoPlist,
      withXcodeProject,
    );
  }

  if (platforms.includes("web")) {
    plugins.push(withWebAssets);
  }

  const licenseKey = process.env["BOOTSPLASH_LICENSE_KEY"];

  if ("licenseKey" in rawProps) {
    log.warn(
      "Passing licenseKey directly in plugin config is deprecated as it could be leaked by expo-constants. Use the BOOTSPLASH_LICENSE_KEY environment variable instead.",
    );
  }

  const rawPropsWithEnvVariable = {
    ...rawProps,
    ...(licenseKey != null && { licenseKey }),
  };

  return Expo.withPlugins(
    expoConfig,
    plugins.map((plugin) => [plugin, rawPropsWithEnvVariable]),
  );
}, PACKAGE_NAME);

export default (
  config: BootSplashPluginConfig,
): [typeof PACKAGE_NAME, BootSplashPluginConfig] => [PACKAGE_NAME, config];
