// Copyright (C) 2026 Acoustic, L.P. All rights reserved.
//
// NOTICE: This file contains material that is confidential and proprietary to
// Acoustic, L.P. and/or other developers. No license is granted under any
// intellectual or industrial property rights of Acoustic, L.P. except as may
// be provided in an agreement with Acoustic, L.P. Any unauthorized copying or
// distribution of content from this file is prohibited.

import {
  withEntitlementsPlist,
  withXcodeProject,
  withDangerousMod,
  type ConfigPlugin,
} from '@expo/config-plugins'
import type { ExpoConfig } from '@expo/config-types'

// XcodeProject is declared in the `xcode` package which ships no TypeScript
// types of its own. We derive the type from the withXcodeProject callback
// so the compiler can still check our usage without requiring @types/xcode.
type XcodeProject = Parameters<
  Parameters<typeof withXcodeProject>[1]
>[0]['modResults']
import * as fs from 'fs'
import * as path from 'path'

// ─── Types ────────────────────────────────────────────────────────────────────

export interface ConnectPluginProps {
  /** Explicit App Group override. Wins over ConnectConfig.json when set. */
  iosAppGroupIdentifier?: string
  /**
   * Apple Team ID (10-char) used to sign the host app and the push extensions.
   * Wins over `Connect.iOSDevelopmentTeam` in ConnectConfig.json when set.
   * Required for push: without a team, a CLI build drops the `aps-environment`
   * entitlement to ad-hoc signing and the OS issues no APNs token.
   */
  iosDevelopmentTeam?: string
}

// ─── App Group resolution ─────────────────────────────────────────────────────

// Apple App Group identifiers are `group.` followed by a reverse-DNS string of
// alphanumerics, dots, and hyphens. Validating against this both catches typos
// early and guarantees the value is safe to interpolate into the entitlements
// XML (no `<`, `>`, `&`, or quotes can appear).
const APP_GROUP_PATTERN = /^group\.[A-Za-z0-9.-]+$/

/**
 * Validates an App Group identifier against Apple's format and returns it.
 * Throws an actionable error otherwise.
 */
export function assertValidAppGroup(value: string, source: string): string {
  if (!APP_GROUP_PATTERN.test(value)) {
    throw new Error(
      `[react-native-acoustic-connect-beta] Invalid iOS App Group identifier ` +
        `"${value}" (from ${source}).\n\n` +
        `It must start with "group." followed by letters, digits, dots, or ` +
        `hyphens — e.g. "group.com.example.app".`
    )
  }
  return value
}

/**
 * Resolves the iOS App Group identifier used by ConnectNSE.
 *
 * Priority:
 *  1. `iosAppGroupIdentifier` plugin prop in app.json (explicit override)
 *  2. `Connect.iOSAppGroupIdentifier` in `<projectRoot>/ConnectConfig.json`
 *
 * The resolved value is validated against Apple's App Group format. Throws a
 * clear, actionable error if neither source provides the value, or if the
 * provided value is malformed.
 */
export function resolveAppGroupIdentifier(
  projectRoot: string,
  props: ConnectPluginProps
): string {
  if (props.iosAppGroupIdentifier) {
    return assertValidAppGroup(
      props.iosAppGroupIdentifier,
      'app.json plugin prop'
    )
  }

  const configPath = path.join(projectRoot, 'ConnectConfig.json')
  if (fs.existsSync(configPath)) {
    const raw = fs.readFileSync(configPath, 'utf8')
    let parsed: Record<string, unknown>
    try {
      parsed = JSON.parse(raw) as Record<string, unknown>
    } catch {
      throw new Error(
        `[react-native-acoustic-connect-beta] ConnectConfig.json at ${configPath} is not valid JSON.`
      )
    }
    const connect = parsed['Connect'] as Record<string, unknown> | undefined
    const groupId = connect?.['iOSAppGroupIdentifier']
    if (typeof groupId === 'string' && groupId.length > 0) {
      return assertValidAppGroup(
        groupId,
        'Connect.iOSAppGroupIdentifier in ConnectConfig.json'
      )
    }
  }

  throw new Error(
    `[react-native-acoustic-connect-beta] Could not resolve iOS App Group identifier.\n\n` +
      `To fix this, do ONE of the following:\n` +
      `  1. (Preferred) Add "iOSAppGroupIdentifier" to the "Connect" object in ` +
      `your project's ConnectConfig.json:\n` +
      `       { "Connect": { "iOSAppGroupIdentifier": "group.com.example.app" } }\n` +
      `  2. Pass the identifier as a plugin prop in app.json:\n` +
      `       ["react-native-acoustic-connect-beta", { "iosAppGroupIdentifier": "group.com.example.app" }]\n`
  )
}

// ─── Swift template substitution ─────────────────────────────────────────────

const PLACEHOLDER = 'CONNECT_APP_GROUP_IDENTIFIER_PLACEHOLDER'
const GENERATED_HEADER =
  '// @generated by react-native-acoustic-connect-beta Expo Config Plugin — do not edit manually.\n'

/**
 * Returns the content of `plugin/swift/NotificationService.swift` with the
 * placeholder replaced by the resolved App Group identifier and a generated
 * header prepended.
 */
export function substituteSwiftTemplate(
  templatePath: string,
  appGroupIdentifier: string
): string {
  const template = fs.readFileSync(templatePath, 'utf8')
  // split/join replaces EVERY occurrence — the template mentions the
  // placeholder in a comment before the code occurrence, and a string
  // pattern to .replace() would only substitute that first mention.
  const substituted = template.split(PLACEHOLDER).join(appGroupIdentifier)
  return GENERATED_HEADER + substituted
}

// ─── Plist helpers ────────────────────────────────────────────────────────────

/**
 * Generates the Info.plist content for the ConnectNSE target.
 *
 * RCTNewArchEnabled is intentionally omitted: the NSE links the Connect SDK
 * only — no React Native runtime is present in the extension process.
 */
export function buildNSEInfoPlist(): string {
  return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
\t<key>CFBundleDevelopmentRegion</key>
\t<string>$(DEVELOPMENT_LANGUAGE)</string>
\t<key>CFBundleDisplayName</key>
\t<string>ConnectNSE</string>
\t<key>CFBundleExecutable</key>
\t<string>$(EXECUTABLE_NAME)</string>
\t<key>CFBundleIdentifier</key>
\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
\t<key>CFBundleInfoDictionaryVersion</key>
\t<string>6.0</string>
\t<key>CFBundleName</key>
\t<string>$(PRODUCT_NAME)</string>
\t<key>CFBundlePackageType</key>
\t<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
\t<key>CFBundleShortVersionString</key>
\t<string>$(MARKETING_VERSION)</string>
\t<key>CFBundleVersion</key>
\t<string>$(CURRENT_PROJECT_VERSION)</string>
\t<key>NSExtension</key>
\t<dict>
\t\t<key>NSExtensionPointIdentifier</key>
\t\t<string>com.apple.usernotifications.service</string>
\t\t<key>NSExtensionPrincipalClass</key>
\t\t<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
\t</dict>
</dict>
</plist>
`
}

/** Generates the entitlements plist content for the ConnectNSE target. */
export function buildNSEEntitlements(appGroupIdentifier: string): string {
  return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
\t<key>com.apple.security.application-groups</key>
\t<array>
\t\t<string>${appGroupIdentifier}</string>
\t</array>
</dict>
</plist>
`
}

// ─── Podfile injection ────────────────────────────────────────────────────────

const PODFILE_MARKER =
  '# @generated ConnectNSE target (react-native-acoustic-connect-beta)'

/**
 * Builds the Ruby Podfile snippet shared by the NSE and NCE mods: a helper
 * that resolves the AcousticConnect pod (name + version requirements) from
 * ConnectConfig.json, plus an extension `target` that links it. Centralised
 * here so the NSE and NCE blocks have a single source of truth for the
 * resolution logic — they differ only in marker, helper name, and target name.
 *
 * Tolerates a missing ConnectConfig.json or a missing `Connect` key: when
 * the file or key is absent the block defaults to AcousticConnectDebug with
 * floor '>= 2.1.12' and no version pin, so prop-only setups (no
 * ConnectConfig.json) keep working.  If the file exists but contains invalid
 * JSON, `pod install` will print a warning and use the same defaults rather
 * than crashing.
 */
export function buildConnectPodTargetBlock(
  marker: string,
  helperName: string,
  targetName: string
): string {
  return `
${marker}
def ${helperName}
  config_path = File.join(__dir__, '..', 'ConnectConfig.json')
  connect_config = {}
  if File.exist?(config_path)
    begin
      parsed = JSON.parse(File.read(config_path))
      connect_config = parsed['Connect'] || {}
    rescue JSON::ParserError => e
      warn "[react-native-acoustic-connect-beta] ConnectConfig.json is not valid JSON: #{e.message}. Using defaults."
    end
  end
  use_release = connect_config['useRelease'] || false
  ios_version = (connect_config['iOSVersion'] || '').to_s
  name = use_release ? 'AcousticConnect' : 'AcousticConnectDebug'
  floor = '>= 2.1.12'
  requirements = ios_version.empty? ? [floor] : [floor, ios_version]
  [name, requirements]
end

target '${targetName}' do
  connect_name, connect_requirements = ${helperName}
  pod connect_name, *connect_requirements
end
# @end ${targetName} target (react-native-acoustic-connect-beta)
`
}

/**
 * Returns the Ruby snippet to inject into the Expo-generated Podfile for the
 * ConnectNSE target. Thin wrapper over {@link buildConnectPodTargetBlock}.
 */
export function buildPodfileBlock(): string {
  return buildConnectPodTargetBlock(
    PODFILE_MARKER,
    'acoustic_connect_pod_nse',
    'ConnectNSE'
  )
}

/**
 * Injects the ConnectNSE Podfile block into `podfileContent` if not already
 * present (idempotent, guarded by PODFILE_MARKER).
 */
export function injectPodfileBlock(podfileContent: string): string {
  if (podfileContent.includes(PODFILE_MARKER)) {
    return podfileContent
  }
  return podfileContent + buildPodfileBlock()
}

// ─── Entitlements mod ─────────────────────────────────────────────────────────

/**
 * Adds the HOST app's push entitlements. Merges idempotently:
 * - `aps-environment` — the APNs (Push Notifications) capability. Required for
 *   the SDK's automatic `registerForRemoteNotifications()` to receive a device
 *   token; without it iOS fails registration with "no valid aps-environment
 *   entitlement string found", so no token is ever sent to the collector.
 *   Mirrors the bare-workflow host entitlements. `development` targets the
 *   sandbox APNs used by dev/`expo run:ios`/EAS `development` builds;
 *   production/TestFlight builds need `production`.
 * - `com.apple.security.application-groups` — shared App Group for the host app
 *   and the NSE/NCE extensions. Never clobbers an existing array.
 */
export function withNSEEntitlements(
  config: ExpoConfig,
  appGroupIdentifier: string
): ExpoConfig {
  return withEntitlementsPlist(config, (c) => {
    if (!c.modResults['aps-environment']) {
      c.modResults['aps-environment'] = 'development'
    }
    const existing: string[] =
      (c.modResults['com.apple.security.application-groups'] as
        | string[]
        | undefined) ?? []
    if (!existing.includes(appGroupIdentifier)) {
      c.modResults['com.apple.security.application-groups'] = [
        ...existing,
        appGroupIdentifier,
      ]
    }
    return c
  })
}

// ─── Xcode project mod ───────────────────────────────────────────────────────

const NSE_TARGET_NAME = 'ConnectNSE'

interface BuildSettings {
  IPHONEOS_DEPLOYMENT_TARGET?: string
  SWIFT_VERSION?: string
  MARKETING_VERSION?: string
  CURRENT_PROJECT_VERSION?: string
  [key: string]: string | undefined
}

/**
 * Extracts a build setting value scoped to the host native target's
 * configuration list for the given `configName` (e.g. "Debug" or "Release").
 * Falls back to the other host config if the named one is absent, then to the
 * hardcoded `fallback`.
 *
 * Scoped to host configs only: configs whose CODE_SIGN_ENTITLEMENTS references
 * one of `skipTargetNames` (the extension targets this plugin generates) are
 * skipped, so values are never mirrored from a previously generated extension
 * target. Defaults to `[NSE_TARGET_NAME]`; the NCE mod (withConnectNCE) passes
 * both ConnectNSE and ConnectNCE so a re-run that already wrote the NSE target
 * never mirrors the sibling extension's settings.
 */
export function getHostBuildSettingForConfig(
  xcodeProject: XcodeProject,
  setting: string,
  configName: string,
  fallback: string,
  skipTargetNames: string[] = [NSE_TARGET_NAME]
): string {
  const allConfigs = xcodeProject.pbxXCBuildConfigurationSection() as Record<
    string,
    { name?: string; buildSettings?: BuildSettings }
  >

  // Two-pass resolution, both passes skipping the plugin's own NSE configs
  // (identified by CODE_SIGN_ENTITLEMENTS containing NSE_TARGET_NAME) so a
  // re-run never mirrors values from a previously generated extension target:
  //   Pass 1 — exact match on the requested configName (Debug → Debug).
  //   Pass 2 — graceful fallback to the sibling host config's value when the
  //            matching config has no explicit setting; mirroring the host's
  //            other config beats dropping to the hardcoded default.

  // Pass 1: exact configName match.
  for (const key of Object.keys(allConfigs)) {
    const entry = allConfigs[key]
    if (
      entry &&
      typeof entry === 'object' &&
      entry.name === configName &&
      entry.buildSettings
    ) {
      // Skip NSE/NCE extension targets already written by this plugin
      const cse = entry.buildSettings['CODE_SIGN_ENTITLEMENTS']
      if (
        typeof cse === 'string' &&
        skipTargetNames.some((name) => cse.includes(name))
      ) {
        continue
      }
      const val = entry.buildSettings[setting]
      if (typeof val === 'string') {
        return val
      }
    }
  }

  // Pass 2: sibling host config fallback (any name), skip NSE/NCE targets.
  for (const key of Object.keys(allConfigs)) {
    const entry = allConfigs[key]
    if (entry && entry.buildSettings) {
      const cse = entry.buildSettings['CODE_SIGN_ENTITLEMENTS']
      if (
        typeof cse === 'string' &&
        skipTargetNames.some((name) => cse.includes(name))
      ) {
        continue
      }
      const val = entry.buildSettings[setting]
      if (typeof val === 'string') {
        return val
      }
    }
  }

  return fallback
}

/**
 * Adds the ConnectNSE Xcode target (app_extension) to the project.
 *
 * Follows the OneSignal plugin pattern (withOneSignalNSE.ts) using the
 * `xcode` API exposed via `config.modResults`.
 *
 * Idempotent: if a target named ConnectNSE already exists, skips target
 * creation but still (re)writes the source files.
 */
export function withNSEXcodeProject(
  config: ExpoConfig,
  appGroupIdentifier: string,
  swiftContent: string
): ExpoConfig {
  return withXcodeProject(config, (c) => {
    const xcodeProject: XcodeProject = c.modResults

    // ios/ directory — modRequest.platformProjectRoot is the canonical
    // native project root. (xcodeProject.filepath points INSIDE the
    // .xcodeproj bundle, so deriving from it misplaces the files.)
    const iosDir = c.modRequest.platformProjectRoot
    const nseDir = path.join(iosDir, NSE_TARGET_NAME)

    // Always write / overwrite the source files (idempotent)
    fs.mkdirSync(nseDir, { recursive: true })

    fs.writeFileSync(
      path.join(nseDir, 'NotificationService.swift'),
      swiftContent,
      'utf8'
    )
    fs.writeFileSync(
      path.join(nseDir, 'Info.plist'),
      buildNSEInfoPlist(),
      'utf8'
    )
    fs.writeFileSync(
      path.join(nseDir, `${NSE_TARGET_NAME}.entitlements`),
      buildNSEEntitlements(appGroupIdentifier),
      'utf8'
    )

    // Idempotency check — skip if target already registered in the pbxproj.
    // Note: pbxTargetByName misses targets on a REPARSED project because the
    // xcode lib stores written names with literal quotes ('"ConnectNSE"'),
    // so compare quote-stripped names across the native-target section.
    const nativeTargets = xcodeProject.pbxNativeTargetSection() as Record<
      string,
      { name?: string } | string
    >
    const targetExists = Object.values(nativeTargets).some(
      (t) =>
        typeof t === 'object' &&
        typeof t.name === 'string' &&
        t.name.replace(/"/g, '') === NSE_TARGET_NAME
    )
    if (targetExists) {
      return c
    }

    // Derive the host bundle id — the NSE bundle id must be
    // `<hostBundleId>.ConnectNSE`. Fail fast rather than emit a placeholder
    // (a wrong bundle id breaks App Group pairing and App Store submission).
    const hostBundleId = c.ios?.bundleIdentifier as string | undefined
    if (!hostBundleId) {
      throw new Error(
        `[react-native-acoustic-connect-beta] ios.bundleIdentifier is not set.\n\n` +
          `The ConnectNSE extension bundle id is derived as ` +
          `"<ios.bundleIdentifier>.ConnectNSE", so the host bundle id must be ` +
          `defined in app.json before prebuild:\n` +
          `       { "expo": { "ios": { "bundleIdentifier": "com.example.app" } } }`
      )
    }

    // Add the native target (app_extension)
    const nseTarget = xcodeProject.addTarget(
      NSE_TARGET_NAME,
      'app_extension',
      NSE_TARGET_NAME,
      `${hostBundleId}.ConnectNSE`
    )

    if (!nseTarget) {
      throw new Error(
        `[react-native-acoustic-connect-beta] Failed to add ConnectNSE Xcode target.`
      )
    }

    // Add the build phase for Swift sources
    xcodeProject.addBuildPhase(
      ['NotificationService.swift'],
      'PBXSourcesBuildPhase',
      'Sources',
      nseTarget.uuid
    )

    // Add Resources build phase (empty, but required by Xcode)
    xcodeProject.addBuildPhase(
      [],
      'PBXResourcesBuildPhase',
      'Resources',
      nseTarget.uuid
    )

    // Add Frameworks build phase (empty — CocoaPods populates it)
    xcodeProject.addBuildPhase(
      [],
      'PBXFrameworksBuildPhase',
      'Frameworks',
      nseTarget.uuid
    )

    // Make the host app target depend on ConnectNSE. CocoaPods resolves an
    // extension's host target through PBXTargetDependency (xcodeproj's
    // host_targets_for_embedded_target), NOT through the embed copy phase —
    // without this edge `pod install` fails with "Unable to find host
    // target(s) for ConnectNSE".
    // The xcode lib SILENTLY no-ops addTargetDependency when these sections
    // are absent from the parsed project (Expo templates ship without any
    // extension, so they are) — create them first.
    const objects = xcodeProject.hash.project.objects
    objects['PBXTargetDependency'] = objects['PBXTargetDependency'] ?? {}
    objects['PBXContainerItemProxy'] = objects['PBXContainerItemProxy'] ?? {}
    const hostTargetUuid = xcodeProject.getFirstTarget().uuid
    xcodeProject.addTargetDependency(hostTargetUuid, [nseTarget.uuid])

    // Create a PBXGroup for ConnectNSE source files
    const nseGroup = xcodeProject.addPbxGroup(
      [
        'NotificationService.swift',
        'Info.plist',
        `${NSE_TARGET_NAME}.entitlements`,
      ],
      NSE_TARGET_NAME,
      NSE_TARGET_NAME
    )

    // Add the group to the main project group
    const mainGroup = xcodeProject.getFirstProject().firstProject.mainGroup
    xcodeProject.addToPbxGroup(nseGroup.uuid, mainGroup)

    // Set per-configuration build settings — mirror each host configuration
    // individually so Debug gets Debug values and Release gets Release values.
    const buildConfigurations = xcodeProject.pbxXCConfigurationList()
    const nseConfigListUuid = nseTarget.pbxNativeTarget.buildConfigurationList

    if (nseConfigListUuid) {
      const configList = buildConfigurations[nseConfigListUuid]
      if (
        configList &&
        Array.isArray(
          (configList as { buildConfigurations?: unknown[] })
            .buildConfigurations
        )
      ) {
        const configs = xcodeProject.pbxXCBuildConfigurationSection()
        for (const entry of (
          configList as { buildConfigurations: Array<{ value: string }> }
        ).buildConfigurations) {
          const buildConfig = configs[entry.value]
          if (buildConfig && buildConfig.buildSettings) {
            // Determine which host config name to mirror (Debug → Debug, etc.)
            const configName: string =
              (buildConfig as { name?: string }).name ?? 'Debug'

            // M2: mirror per-configuration values; fall back to the other
            // config's value only when the matching one is absent.
            const deploymentTarget = getHostBuildSettingForConfig(
              xcodeProject,
              'IPHONEOS_DEPLOYMENT_TARGET',
              configName,
              // Matches the AcousticConnect podspec floor (iOS >= 15.1) so the
              // NSE never demands a higher minimum than the host app.
              '15.1'
            )
            const swiftVersion = getHostBuildSettingForConfig(
              xcodeProject,
              'SWIFT_VERSION',
              configName,
              '5.0'
            )
            const marketingVersion = getHostBuildSettingForConfig(
              xcodeProject,
              'MARKETING_VERSION',
              configName,
              '1.0'
            )
            const currentProjectVersion = getHostBuildSettingForConfig(
              xcodeProject,
              'CURRENT_PROJECT_VERSION',
              configName,
              '1'
            )

            Object.assign(buildConfig.buildSettings, {
              // C1: prevent Xcode from synthesising a second Info.plist that
              // would shadow the NSExtension dictionary in ours.
              GENERATE_INFOPLIST_FILE: 'NO',
              INFOPLIST_FILE: `${NSE_TARGET_NAME}/Info.plist`,
              // M1: extension must target the device SDK and restrict to
              // extension-safe APIs (mirrors bare-workflow reference target).
              SDKROOT: 'iphoneos',
              APPLICATION_EXTENSION_API_ONLY: 'YES',
              IPHONEOS_DEPLOYMENT_TARGET: deploymentTarget,
              SWIFT_VERSION: swiftVersion,
              MARKETING_VERSION: marketingVersion,
              CURRENT_PROJECT_VERSION: currentProjectVersion,
              CODE_SIGN_ENTITLEMENTS: `${NSE_TARGET_NAME}/${NSE_TARGET_NAME}.entitlements`,
              PRODUCT_NAME: NSE_TARGET_NAME,
              PRODUCT_BUNDLE_IDENTIFIER: `${hostBundleId}.ConnectNSE`,
              TARGETED_DEVICE_FAMILY: '"1,2"',
              CODE_SIGN_STYLE: 'Automatic',
            })
          }
        }
      }
    }

    return c
  })
}

// ─── Podfile mod ─────────────────────────────────────────────────────────────

/**
 * Injects the ConnectNSE Podfile target block via withDangerousMod.
 * Guarded by a marker comment — re-runs are no-ops.
 *
 * M5: throws a clear, actionable error when the Podfile does not exist at
 * mod-execution time, instead of silently returning — a silent skip would
 * ship an NSE whose `import Connect` cannot resolve.
 */
export function withNSEPodfile(config: ExpoConfig): ExpoConfig {
  return withDangerousMod(config, [
    'ios',
    async (c) => {
      const podfilePath = path.join(c.modRequest.platformProjectRoot, 'Podfile')
      if (!fs.existsSync(podfilePath)) {
        throw new Error(
          `[react-native-acoustic-connect-beta] ios/Podfile not found at ${podfilePath}.\n\n` +
            `This mod runs after prebuild generates the ios/ directory. ` +
            `If you are running this plugin outside of \`expo prebuild\`, ` +
            `ensure the Podfile exists before the dangerous mod phase executes.\n` +
            `Check prebuild ordering: withDangerousMod('ios') runs after ` +
            `withXcodeProject, so the ios/ directory should already be present.`
        )
      }
      const current = fs.readFileSync(podfilePath, 'utf8')
      const updated = injectPodfileBlock(current)
      if (updated !== current) {
        fs.writeFileSync(podfilePath, updated, 'utf8')
      }
      return c
    },
  ])
}

// ─── Composed mod ─────────────────────────────────────────────────────────────

/**
 * Expo Config Plugin mod that provisions a Notification Service Extension
 * (NSE) Xcode target named `ConnectNSE`.
 *
 * Applies three mutations — all idempotent:
 *  1. Host app entitlements: merges App Group into
 *     `com.apple.security.application-groups`.
 *  2. Xcode project: adds ConnectNSE target + files (skips if already present).
 *  3. Podfile: injects `target 'ConnectNSE'` block (guarded by marker comment).
 *
 * M4: `config._internal.projectRoot` is required. If absent (i.e. invoked
 * outside Expo CLI), an actionable error is thrown rather than silently
 * falling back to `process.cwd()`, which would misresolve ConnectConfig.json
 * in monorepo setups.
 */
export const withConnectNSE: ConfigPlugin<ConnectPluginProps> = (
  config,
  props = {}
) => {
  const projectRoot = config._internal?.projectRoot
  if (!projectRoot) {
    throw new Error(
      `[react-native-acoustic-connect-beta] config._internal.projectRoot is not set.\n\n` +
        `This value is injected by Expo CLI during \`expo prebuild\`. ` +
        `If you are calling this plugin outside of Expo CLI (e.g. in a test or ` +
        `custom script), set config._internal = { projectRoot: '/abs/path/to/project' } ` +
        `before invoking the plugin.`
    )
  }

  const appGroupIdentifier = resolveAppGroupIdentifier(projectRoot, props)

  const swiftTemplatePath = path.join(
    __dirname,
    '..',
    'swift',
    'NotificationService.swift'
  )
  const swiftContent = substituteSwiftTemplate(
    swiftTemplatePath,
    appGroupIdentifier
  )

  let result = withNSEEntitlements(config, appGroupIdentifier)
  result = withNSEXcodeProject(result, appGroupIdentifier, swiftContent)
  result = withNSEPodfile(result)
  return result
}
