import {
  ConfigPlugin,
  withXcodeProject,
  XcodeProject,
  ExportedConfigWithProps,
} from "@expo/config-plugins";

import {
  addResourceFileToGroup,
  getProjectName,
} from "@expo/config-plugins/build/ios/utils/Xcodeproj";

import plist from "@expo/plist";
import * as fs from "fs";
import * as path from "path";

export type PrivacyInfo = {
  NSPrivacyAccessedAPITypes: {
    NSPrivacyAccessedAPIType: string;
    NSPrivacyAccessedAPITypeReasons: string[];
  }[];
  NSPrivacyCollectedDataTypes: {
    NSPrivacyCollectedDataType: string;
    NSPrivacyCollectedDataTypeLinked: boolean;
    NSPrivacyCollectedDataTypeTracking: boolean;
    NSPrivacyCollectedDataTypePurposes: string[];
  }[];
  NSPrivacyTracking: boolean;
  NSPrivacyTrackingDomains: string[];
};

export function setPrivacyInfo(
  projectConfig: ExportedConfigWithProps<XcodeProject>,
  privacyManifests: Partial<PrivacyInfo>
) {
  const { projectRoot, platformProjectRoot } = projectConfig.modRequest;

  const projectName = getProjectName(projectRoot);

  const privacyFilePath = path.join(
    platformProjectRoot,
    projectName,
    "PrivacyInfo.xcprivacy"
  );

  const existingFileContent = getFileContents(privacyFilePath);

  const parsedContent = existingFileContent
    ? plist.parse(existingFileContent)
    : {};
  const mergedContent = mergePrivacyInfo(parsedContent, privacyManifests);
  const contents = plist.build(mergedContent);

  ensureFileExists(privacyFilePath, contents);

  if (!projectConfig.modResults.hasFile(privacyFilePath)) {
    projectConfig.modResults = addResourceFileToGroup({
      filepath: path.join(projectName, "PrivacyInfo.xcprivacy"),
      groupName: projectName,
      project: projectConfig.modResults,
      isBuildFile: true,
      verbose: true,
    });
  }

  return projectConfig;
}

function getFileContents(filePath: string): string | null {
  if (!fs.existsSync(filePath)) {
    return null;
  }
  return fs.readFileSync(filePath, { encoding: "utf8" });
}

function ensureFileExists(filePath: string, contents: string) {
  if (!fs.existsSync(path.dirname(filePath))) {
    fs.mkdirSync(path.dirname(filePath), { recursive: true });
  }
  fs.writeFileSync(filePath, contents);
}

export function mergePrivacyInfo(
  existing: Partial<PrivacyInfo>,
  privacyManifests: Partial<PrivacyInfo>
): PrivacyInfo {
  let {
    NSPrivacyAccessedAPITypes = [],
    NSPrivacyCollectedDataTypes = [],
    NSPrivacyTracking = false,
    NSPrivacyTrackingDomains = [],
  } = structuredClone(existing);
  // tracking is a boolean, so we can just overwrite it
  NSPrivacyTracking =
    privacyManifests.NSPrivacyTracking ?? existing.NSPrivacyTracking ?? false;
  // merge the api types – for each type ensure the key is in the array, and if it is add the reason if it's not there
  privacyManifests.NSPrivacyAccessedAPITypes?.forEach((newType) => {
    const existingType = NSPrivacyAccessedAPITypes.find(
      (t) => t.NSPrivacyAccessedAPIType === newType.NSPrivacyAccessedAPIType
    );
    if (!existingType) {
      NSPrivacyAccessedAPITypes.push(newType);
    } else {
      existingType.NSPrivacyAccessedAPITypeReasons = [
        ...new Set(
          existingType?.NSPrivacyAccessedAPITypeReasons?.concat(
            ...newType.NSPrivacyAccessedAPITypeReasons
          )
        ),
      ];
    }
  });
  // merge the collected data types – for each type ensure the key is in the array, and if it is add the purposes if it's not there
  privacyManifests.NSPrivacyCollectedDataTypes?.forEach((newType) => {
    const existingType = NSPrivacyCollectedDataTypes.find(
      (t) => t.NSPrivacyCollectedDataType === newType.NSPrivacyCollectedDataType
    );
    if (!existingType) {
      NSPrivacyCollectedDataTypes.push(newType);
    } else {
      existingType.NSPrivacyCollectedDataTypePurposes = [
        ...new Set(
          existingType?.NSPrivacyCollectedDataTypePurposes?.concat(
            ...newType.NSPrivacyCollectedDataTypePurposes
          )
        ),
      ];
    }
  });
  // merge the tracking domains
  NSPrivacyTrackingDomains = [
    ...new Set(
      NSPrivacyTrackingDomains.concat(
        privacyManifests.NSPrivacyTrackingDomains ?? []
      )
    ),
  ];

  return {
    NSPrivacyAccessedAPITypes,
    NSPrivacyCollectedDataTypes,
    NSPrivacyTracking,
    NSPrivacyTrackingDomains,
  };
}

const withExpoPrivacyManifestPolyfillPlugin: ConfigPlugin<{} | void> = (
  config,
  _props = {}
) => {
  const privacyManifests = config.ios?.privacyManifests;
  if (!privacyManifests) {
    return config;
  }

  return withXcodeProject(
    config,
    (projectConfig: ExportedConfigWithProps<XcodeProject>) => {
      return setPrivacyInfo(projectConfig, privacyManifests);
    }
  );
};

export default withExpoPrivacyManifestPolyfillPlugin;
