CountyCollective Reference

Beacon GIS Boundary Import

Original Beacon GIS layer, WKT conversion, generated file, composite, and manifest logic.

Back to CountyCollective Reference

wordpress-plugin/includes/gis/class-cgop-gis-boundary-import.php

<?php
/**
 * County GOP Core — GIS Beacon boundary import and GeoJSON generator.
 *
 * Extracted from theme/cgop-theme/inc/gis-boundary-import.php in Pass 08.
 * Regenerates local GeoJSON files from configured Beacon vector layers. This is
 * an admin-only import workflow; public pages consume generated static files.
 *
 * Python WKT converter moved from theme/tools/gis/ to plugin/tools/gis/ per
 * Pass 08 decision P08-001-A.
 *
 * Option keys, function names, admin slugs, and action names are frozen legacy
 * identifiers — do not rename without explicit owner approval.
 * GIS-001 county option scoping is deferred; options remain site-global.
 *
 * @package CountyGOPCore
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

define( 'CGOP_GIS_IMPORT_CAP', 'manage_options' );
define( 'CGOP_GIS_IMPORT_SETTINGS_OPTION', 'CGOP_gis_boundary_import_settings' );
define( 'CGOP_GIS_IMPORT_STATUS_OPTION', 'CGOP_gis_boundary_import_status' );
define( 'CGOP_GIS_IMPORT_CUSTOM_LAYERS_OPTION', 'CGOP_gis_boundary_import_custom_layers' );
define( 'CGOP_GIS_COMPOSITE_MAPS_OPTION', 'CGOP_gis_composite_maps' );
define( 'CGOP_GIS_DIRECT_IMPORTS_OPTION', 'CGOP_gis_direct_geojson_imports' );

/**
 * Return the configured Beacon layer registry.
 *
 * @return array[]
 */
function CGOP_gis_get_layer_registry() {
	return CGOP_gis_get_custom_layer_registry();
}

/**
 * Return administrator-added Beacon layers.
 *
 * @return array[]
 */
function CGOP_gis_get_custom_layer_registry() {
	$custom = get_option( CGOP_GIS_IMPORT_CUSTOM_LAYERS_OPTION, array() );
	return is_array( $custom ) ? $custom : array();
}

/**
 * Generate a unique internal Beacon layer ID.
 *
 * @return string
 */
function CGOP_gis_generate_layer_id() {
	$existing = CGOP_gis_get_custom_layer_registry();

	for ( $i = 0; $i < 20; $i++ ) {
		$id = wp_generate_uuid4();
		if ( ! isset( $existing[ $id ] ) ) {
			return $id;
		}
	}

	return 'layer-' . wp_generate_uuid4();
}

/**
 * Normalize a Beacon layer configuration.
 *
 * @param array $raw Raw layer data.
 * @return array
 */
function CGOP_gis_normalize_layer_config( array $raw ) {
	$label      = sanitize_text_field( $raw['label'] ?? '' );
	$id         = ! empty( $raw['id'] ) ? sanitize_text_field( $raw['id'] ) : CGOP_gis_generate_layer_id();
	$output_dir = ! empty( $raw['output_dir'] ) ? sanitize_title( $raw['output_dir'] ) : sanitize_title( $label );
	if ( ! $output_dir ) {
		$output_dir = sanitize_title( $id );
	}

	return array(
		'id'                     => $id,
		'label'                  => $label,
		'beacon_layer_id'        => absint( $raw['beacon_layer_id'] ?? 0 ),
		'position_type'          => sanitize_key( $raw['position_type'] ?? '' ),
		'level'                  => sanitize_key( $raw['level'] ?? '' ),
		'output_dir'             => $output_dir,
		'preferred_fields'       => CGOP_gis_normalize_preferred_fields( $raw['preferred_fields'] ?? '' ),
		'position_name_template' => sanitize_text_field( $raw['position_name_template'] ?? '{label}' ),
		'position_id_template'   => sanitize_text_field( $raw['position_id_template'] ?? $id . '-{label_slug}' ),
	);
}

/**
 * Normalize preferred field names.
 *
 * @param string|array $fields Field names.
 * @return string[]
 */
function CGOP_gis_normalize_preferred_fields( $fields ) {
	if ( is_array( $fields ) ) {
		$parts = $fields;
	} else {
		$parts = preg_split( '/[\r\n,]+/', (string) $fields );
	}

	$clean = array();
	foreach ( $parts as $field ) {
		$field = trim( sanitize_text_field( $field ) );
		if ( '' !== $field ) {
			$clean[] = $field;
		}
	}

	return $clean ? array_values( array_unique( $clean ) ) : array( 'DisplayKey' );
}

/**
 * Return temporary Beacon request values for the current admin user.
 *
 * @return array
 */
function CGOP_gis_get_request_memory() {
	$memory = get_transient( 'CGOP_gis_request_memory_' . get_current_user_id() );
	if ( ! is_array( $memory ) ) {
		return array(
			'qps'        => '',
			'cookie'     => '',
			'user_agent' => CGOP_gis_get_default_user_agent(),
			'overrides'  => '',
		);
	}

	return wp_parse_args(
		$memory,
		array(
			'qps'        => '',
			'cookie'     => '',
			'user_agent' => CGOP_gis_get_default_user_agent(),
			'overrides'  => '',
		)
	);
}

/**
 * Store temporary Beacon request values for the current admin user.
 *
 * @param string $qps             Beacon QPS value.
 * @param string $cookie          Beacon Cookie header.
 * @param string $user_agent      Browser User-Agent.
 * @param string $overrides       Feature field override lines.
 * @return void
 */
function CGOP_gis_set_request_memory( $qps, $cookie, $user_agent, $overrides = '' ) {
	set_transient(
		'CGOP_gis_request_memory_' . get_current_user_id(),
		array(
			'qps'        => sanitize_text_field( $qps ),
			'cookie'     => CGOP_gis_normalize_cookie_header( $cookie ),
			'user_agent' => sanitize_text_field( $user_agent ),
			'overrides'  => sanitize_textarea_field( $overrides ),
		),
		2 * HOUR_IN_SECONDS
	);
}

/**
 * Return generated GeoJSON file choices from the last import status.
 *
 * @return array[]
 */
function CGOP_gis_get_generated_file_choices() {
	$status  = get_option( CGOP_GIS_IMPORT_STATUS_OPTION, array() );
	$choices = array();

	if ( ! empty( $status['layers'] ) && is_array( $status['layers'] ) ) {
		foreach ( $status['layers'] as $layer ) {
			if ( empty( $layer['files'] ) || ! is_array( $layer['files'] ) ) {
				continue;
			}
			foreach ( $layer['files'] as $file ) {
				if ( empty( $file['file'] ) ) {
					continue;
				}
				$relative             = ltrim( (string) $file['file'], '/' );
				$choices[ $relative ] = array(
					'file'        => $relative,
					'url'         => CGOP_gis_get_generated_file_url( $relative ),
					'label'       => $file['label'] ?? $file['position_id'] ?? basename( $relative ),
					'position_id' => $file['position_id'] ?? '',
					'layer_label' => $layer['label'] ?? '',
				);
			}
		}
	}

	foreach ( CGOP_gis_get_composite_maps() as $composite ) {
		if ( empty( $composite['output_file'] ) || empty( $composite['id'] ) ) {
			continue;
		}
		$relative               = ltrim( (string) $composite['output_file'], '/' );
		$choice_key             = 'composite:' . sanitize_text_field( (string) $composite['id'] );
		$choices[ $choice_key ] = array(
			'file'         => $relative,
			'url'          => CGOP_gis_get_generated_file_url( $relative ),
			'label'        => $composite['label'] ?? basename( $relative ),
			'position_id'  => '',
			'layer_label'  => __( 'Composites', 'county-gop-core' ),
			'is_composite' => true,
			'composite_id' => (string) $composite['id'],
		);
	}

	foreach ( CGOP_gis_get_direct_geojson_imports() as $import ) {
		if ( empty( $import['output_file'] ) || empty( $import['id'] ) ) {
			continue;
		}
		$relative               = ltrim( (string) $import['output_file'], '/' );
		$choice_key             = 'direct:' . sanitize_text_field( (string) $import['id'] );
		$choices[ $choice_key ] = array(
			'file'        => $relative,
			'url'         => CGOP_gis_get_generated_file_url( $relative ),
			'label'       => $import['label'] ?? basename( $relative ),
			'position_id' => '',
			'layer_label' => __( 'Uploaded GeoJSON', 'county-gop-core' ),
			'is_direct'   => true,
			'direct_id'   => (string) $import['id'],
		);
	}

	uasort(
		$choices,
		function ( $a, $b ) {
			return strnatcasecmp( ( $a['layer_label'] . ' ' . $a['label'] ), ( $b['layer_label'] . ' ' . $b['label'] ) );
		}
	);

	return apply_filters( 'CGOP_gis_generated_file_choices', $choices );
}

/**
 * Return composite map definitions.
 *
 * @return array[]
 */
function CGOP_gis_get_composite_maps() {
	$composites = get_option( CGOP_GIS_COMPOSITE_MAPS_OPTION, array() );
	return is_array( $composites ) ? $composites : array();
}

/**
 * Return direct uploaded GeoJSON import definitions.
 *
 * @return array[]
 */
function CGOP_gis_get_direct_geojson_imports() {
	$imports = get_option( CGOP_GIS_DIRECT_IMPORTS_OPTION, array() );
	return is_array( $imports ) ? $imports : array();
}

/**
 * Generate a unique ID for a direct uploaded GeoJSON import.
 *
 * @return string
 */
function CGOP_gis_generate_direct_import_id() {
	$existing = CGOP_gis_get_direct_geojson_imports();
	for ( $i = 0; $i < 20; $i++ ) {
		$id = wp_generate_uuid4();
		if ( ! isset( $existing[ $id ] ) ) {
			return $id;
		}
	}
	return 'direct-' . wp_generate_uuid4();
}

/**
 * Generate a unique ID for a new composite map.
 *
 * @return string
 */
function CGOP_gis_generate_composite_id() {
	$existing = CGOP_gis_get_composite_maps();
	for ( $i = 0; $i < 20; $i++ ) {
		$id = wp_generate_uuid4();
		if ( ! isset( $existing[ $id ] ) ) {
			return $id;
		}
	}
	return 'composite-' . wp_generate_uuid4();
}

/**
 * Return an unused composite output slug.
 *
 * Composite dropdown values are now keyed by UUID, but the generated files
 * still need unique paths so one composite cannot overwrite another.
 *
 * @param string  $slug         Desired slug.
 * @param string  $composite_id Composite ID being created or updated.
 * @param array[] $composites   Existing composite definitions.
 * @return string
 */
function CGOP_gis_get_unique_composite_slug( $slug, $composite_id, $composites ) {
	$base = CGOP_gis_slug( $slug );
	if ( '' === $base ) {
		$base = 'composite';
	}

	$used = array();
	foreach ( $composites as $existing_id => $composite ) {
		if ( (string) $existing_id === (string) $composite_id ) {
			continue;
		}
		if ( empty( $composite['output_file'] ) ) {
			continue;
		}
		$used[ ltrim( (string) $composite['output_file'], '/' ) ] = true;
	}

	$candidate = $base;
	$suffix    = substr( preg_replace( '/[^a-zA-Z0-9]/', '', (string) $composite_id ), 0, 8 );
	if ( '' === $suffix ) {
		$suffix = (string) time();
	}

	for ( $i = 0; $i < 100; $i++ ) {
		$output_rel = 'composites/' . $candidate . '.geojson';
		if ( ! isset( $used[ $output_rel ] ) ) {
			return $candidate;
		}
		$candidate = 0 === $i ? $base . '-' . strtolower( $suffix ) : $base . '-' . strtolower( $suffix ) . '-' . ( $i + 1 );
	}

	return $base . '-' . strtolower( $suffix ) . '-' . time();
}

/**
 * Return whether another composite still references an output file.
 *
 * @param string  $output_file  Relative output file path.
 * @param array[] $composites   Composite definitions.
 * @param string  $exclude_id   Composite ID to ignore.
 * @return bool
 */
function CGOP_gis_composite_output_is_shared( $output_file, $composites, $exclude_id = '' ) {
	$output_file = ltrim( (string) $output_file, '/' );
	if ( '' === $output_file ) {
		return false;
	}

	foreach ( $composites as $composite_id => $composite ) {
		if ( '' !== $exclude_id && (string) $composite_id === (string) $exclude_id ) {
			continue;
		}
		if ( ltrim( (string) ( $composite['output_file'] ?? '' ), '/' ) === $output_file ) {
			return true;
		}
	}

	return false;
}

/**
 * Clear Position Key map assignments that point at a deleted composite.
 *
 * @param string $composite_id   Deleted composite ID.
 * @param string $output_file    Deleted composite output file.
 * @param bool   $clear_url_refs Whether URL references to the output file are safe to clear.
 * @return int Number of map boundary rows deleted.
 */
function CGOP_gis_clear_deleted_composite_assignments( $composite_id, $output_file, $clear_url_refs ) {
	global $wpdb;

	$refs = array( 'composite:' . (string) $composite_id );
	if ( $clear_url_refs && $output_file ) {
		$refs[] = CGOP_gis_get_generated_file_url( $output_file );
	}
	$refs = array_values( array_unique( array_filter( array_map( 'strval', $refs ) ) ) );
	if ( ! $refs ) {
		return 0;
	}

	$map_table      = CGOP_map_boundaries_table();
	$position_table = CGOP_position_keys_table();
	$deleted        = 0;

	// County scope for position_keys updates.
	$pk_scope = function_exists( 'cgop_get_table_county_scope' ) ? cgop_get_table_county_scope( $position_table ) : false;
	if ( null === $pk_scope ) {
		return 0; // Column exists but no county context — skip to avoid cross-county writes.
	}

	foreach ( $refs as $ref ) {
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$map_ids = $wpdb->get_col(
			$wpdb->prepare( "SELECT map_id FROM `{$map_table}` WHERE json_file = %s", $ref )
		);

		foreach ( $map_ids as $map_id ) {
			if ( false !== $pk_scope ) {
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
				$wpdb->update( $position_table, array( 'live_in_map' => '' ), array( 'live_in_map' => $map_id, 'county_id' => $pk_scope ), array( '%s' ), array( '%s', '%s' ) );
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
				$wpdb->update( $position_table, array( 'voting_area_map' => '' ), array( 'voting_area_map' => $map_id, 'county_id' => $pk_scope ), array( '%s' ), array( '%s', '%s' ) );
			} else {
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
				$wpdb->update( $position_table, array( 'live_in_map' => '' ), array( 'live_in_map' => $map_id ), array( '%s' ), array( '%s' ) );
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
				$wpdb->update( $position_table, array( 'voting_area_map' => '' ), array( 'voting_area_map' => $map_id ), array( '%s' ), array( '%s' ) );
			}

			if ( CGOP_delete_map_boundary( $map_id ) ) {
				$deleted++;
			}
		}
	}

	CGOP_position_keys_clear_cache();

	return $deleted;
}

/**
 * Return whether a GeoJSON geometry can be rendered as a boundary.
 *
 * @param mixed $geometry GeoJSON geometry array.
 * @return bool
 */
function CGOP_gis_is_boundary_geometry( $geometry ) {
	if ( ! is_array( $geometry ) || empty( $geometry['type'] ) || empty( $geometry['coordinates'] ) ) {
		return false;
	}

	return in_array( $geometry['type'], array( 'Polygon', 'MultiPolygon' ), true );
}

/**
 * Return generated file choices that may serve as composite source files.
 *
 * Returns only Beacon-layer files (not composites themselves) to keep the
 * source selection straightforward.
 *
 * @return array[]
 */
function CGOP_gis_get_composite_source_choices() {
	$status  = get_option( CGOP_GIS_IMPORT_STATUS_OPTION, array() );
	$choices = array();

	if ( empty( $status['layers'] ) || ! is_array( $status['layers'] ) ) {
		return $choices;
	}

	foreach ( $status['layers'] as $layer ) {
		if ( empty( $layer['files'] ) || ! is_array( $layer['files'] ) ) {
			continue;
		}
		foreach ( $layer['files'] as $file ) {
			if ( empty( $file['file'] ) ) {
				continue;
			}
			$relative            = ltrim( (string) $file['file'], '/' );
			$choices[ $relative ] = array(
				'file'        => $relative,
				'label'       => $file['label'] ?? $file['position_id'] ?? basename( $relative ),
				'position_id' => $file['position_id'] ?? '',
				'layer_label' => $layer['label'] ?? '',
			);
		}
	}

	uasort(
		$choices,
		function ( $a, $b ) {
			return strnatcasecmp( ( $a['layer_label'] . ' ' . $a['label'] ), ( $b['layer_label'] . ' ' . $b['label'] ) );
		}
	);

	return $choices;
}

/**
 * Validate a single composite source file path.
 *
 * @param string $relative Relative path inside the generated directory.
 * @return WP_Error|null Null on pass; WP_Error on failure.
 */
function CGOP_gis_validate_composite_source_file( $relative ) {
	if ( '' === $relative || str_contains( $relative, '..' ) ) {
		return new WP_Error( 'composite_invalid_path', __( 'Source file path is invalid.', 'county-gop-core' ) );
	}
	if ( ! preg_match( '/\.(geojson|json)$/i', $relative ) ) {
		return new WP_Error( 'composite_invalid_ext', __( 'Source files must have a .geojson or .json extension.', 'county-gop-core' ) );
	}
	if ( 'manifest.json' === basename( $relative ) ) {
		return new WP_Error( 'composite_manifest', __( 'manifest.json cannot be used as a source file.', 'county-gop-core' ) );
	}

	$base      = wp_normalize_path( trailingslashit( CGOP_gis_get_generated_dir() ) );
	$file_path = wp_normalize_path( $base . $relative );
	if ( ! str_starts_with( $file_path, $base ) ) {
		return new WP_Error( 'composite_outside_dir', __( 'Source file is outside the generated maps directory.', 'county-gop-core' ) );
	}

	return null;
}

/**
 * Build composite GeoJSON from a set of source files.
 *
 * Supports FeatureCollection, Feature, Polygon, and MultiPolygon source shapes.
 * Adds composite metadata to each output feature without destroying source properties.
 *
 * @param string   $composite_id Composite ID.
 * @param string   $label        Human-readable composite label.
 * @param string[] $source_files Relative source file paths.
 * @return array|WP_Error GeoJSON FeatureCollection array on success.
 */
function CGOP_gis_build_composite_geojson( $composite_id, $label, $source_files ) {
	$base     = wp_normalize_path( trailingslashit( CGOP_gis_get_generated_dir() ) );
	$features = array();

	foreach ( $source_files as $relative ) {
		$relative  = ltrim( (string) $relative, '/' );
		$file_path = wp_normalize_path( $base . $relative );

		if ( ! str_starts_with( $file_path, $base ) ) {
			return new WP_Error( 'composite_outside_dir', sprintf(
				/* translators: %s: relative file path. */
				__( 'Source file is outside the generated maps directory: %s', 'county-gop-core' ),
				$relative
			) );
		}

		if ( ! file_exists( $file_path ) ) {
			return new WP_Error( 'composite_source_missing', sprintf(
				/* translators: %s: relative file path. */
				__( 'Source file not found: %s', 'county-gop-core' ),
				$relative
			) );
		}

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		$contents = file_get_contents( $file_path );
		if ( false === $contents ) {
			return new WP_Error( 'composite_read_failed', sprintf(
				/* translators: %s: relative file path. */
				__( 'Could not read source file: %s', 'county-gop-core' ),
				$relative
			) );
		}

		$geojson = json_decode( $contents, true );
		if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $geojson ) ) {
			return new WP_Error( 'composite_invalid_json', sprintf(
				/* translators: %s: relative file path. */
				__( 'Source file is not valid JSON: %s', 'county-gop-core' ),
				$relative
			) );
		}

		if ( ! isset( $geojson['type'] ) ) {
			return new WP_Error( 'composite_no_type', sprintf(
				/* translators: %s: relative file path. */
				__( 'Source file has no GeoJSON type: %s', 'county-gop-core' ),
				$relative
			) );
		}

		$meta = array(
			'composite_id'          => $composite_id,
			'composite_label'       => $label,
			'composite_source_file' => $relative,
		);

		$source_features = array();
		$type            = $geojson['type'];

		if ( 'FeatureCollection' === $type ) {
			if ( ! is_array( $geojson['features'] ?? null ) ) {
				continue;
			}
			$source_features = $geojson['features'];
		} elseif ( 'Feature' === $type ) {
			$source_features = array( $geojson );
		} elseif ( 'Polygon' === $type || 'MultiPolygon' === $type ) {
			$source_features = array(
				array( 'type' => 'Feature', 'properties' => array(), 'geometry' => $geojson ),
			);
		} else {
			return new WP_Error( 'composite_unsupported_type', sprintf(
				/* translators: 1: GeoJSON type. 2: file path. */
				__( 'Unsupported GeoJSON type %1$s in source file: %2$s', 'county-gop-core' ),
				$type,
				$relative
			) );
		}

		foreach ( $source_features as $feature ) {
			if ( ! is_array( $feature ) ) {
				continue;
			}
			$geometry = $feature['geometry'] ?? null;
			if ( ! CGOP_gis_is_boundary_geometry( $geometry ) ) {
				continue;
			}
			$props      = is_array( $feature['properties'] ?? null ) ? $feature['properties'] : array();
			$props      = array_merge( $props, $meta );
			$features[] = array(
				'type'       => 'Feature',
				'properties' => $props,
				'geometry'   => $geometry,
			);
		}
	}

	if ( ! $features ) {
		return new WP_Error( 'composite_no_features', __( 'Composite map has no valid Polygon or MultiPolygon boundary features.', 'county-gop-core' ) );
	}

	return array(
		'type'       => 'FeatureCollection',
		'properties' => array(
			'composite_id' => $composite_id,
			'label'        => $label,
			'source_files' => array_values( $source_files ),
		),
		'features'   => $features,
	);
}

/**
 * Build, write, and register a composite GeoJSON file.
 *
 * @param string       $composite_id        Composite ID (new or existing).
 * @param string       $label               Human-readable label.
 * @param string       $slug                Output filename slug (no extension).
 * @param string[]     $source_files        Relative source file paths.
 * @param array[]|null $existing_composites Current composites array, or null to read from option.
 * @return true|WP_Error
 */
function CGOP_gis_save_composite( $composite_id, $label, $slug, $source_files, $existing_composites = null ) {
	$output_rel     = 'composites/' . $slug . '.geojson';
	$generated      = CGOP_gis_get_generated_dir();
	$composites_dir = trailingslashit( $generated ) . 'composites';

	if ( ! wp_mkdir_p( $composites_dir ) ) {
		return new WP_Error( 'composite_mkdir_failed', __( 'Could not create the composites output directory.', 'county-gop-core' ) );
	}

	$geojson = CGOP_gis_build_composite_geojson( $composite_id, $label, $source_files );
	if ( is_wp_error( $geojson ) ) {
		return $geojson;
	}

	$file_path = trailingslashit( $generated ) . $output_rel;
	$json      = wp_json_encode( $geojson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
	if ( ! $json || false === file_put_contents( $file_path, $json ) ) {
		return new WP_Error( 'composite_write_failed', sprintf(
			/* translators: %s: composite output file path. */
			__( 'Could not write composite GeoJSON file: %s', 'county-gop-core' ),
			$output_rel
		) );
	}

	if ( null === $existing_composites ) {
		$existing_composites = CGOP_gis_get_composite_maps();
	}

	$now = current_time( 'mysql' );
	if ( isset( $existing_composites[ $composite_id ] ) ) {
		$existing_composites[ $composite_id ]['label']        = $label;
		$existing_composites[ $composite_id ]['output_file']  = $output_rel;
		$existing_composites[ $composite_id ]['source_files'] = $source_files;
		$existing_composites[ $composite_id ]['updated_at']   = $now;
	} else {
		$existing_composites[ $composite_id ] = array(
			'id'           => $composite_id,
			'label'        => $label,
			'output_file'  => $output_rel,
			'source_files' => $source_files,
			'created_at'   => $now,
			'updated_at'   => $now,
		);
	}

	update_option( CGOP_GIS_COMPOSITE_MAPS_OPTION, $existing_composites, false );

	$status          = get_option( CGOP_GIS_IMPORT_STATUS_OPTION, array() );
	$manifest_result = CGOP_gis_write_manifest( is_array( $status ) ? $status : array() );
	if ( is_wp_error( $manifest_result ) ) {
		return $manifest_result;
	}

	return true;
}

/**
 * Default Beacon request settings.
 *
 * @return array
 */
function CGOP_gis_get_default_settings() {
	return array(
		'beacon_endpoint' => 'https://beacon.schneidercorp.com/api/beaconCore/GetVectorLayer',
		'minx'            => 480000,
		'miny'            => 2190000,
		'maxx'            => 570000,
		'maxy'            => 2295000,
		'feature_limit'   => 1000,
	);
}

/**
 * Return a default browser User-Agent for Beacon AJAX requests.
 *
 * @return string
 */
function CGOP_gis_get_default_user_agent() {
	return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36';
}

/**
 * Get saved settings merged with defaults.
 *
 * @return array
 */
function CGOP_gis_get_settings() {
	$saved = get_option( CGOP_GIS_IMPORT_SETTINGS_OPTION, array() );
	return wp_parse_args( is_array( $saved ) ? $saved : array(), CGOP_gis_get_default_settings() );
}

/**
 * Register admin page under GOP Setup menu.
 */
function CGOP_gis_register_admin_page() {
	add_submenu_page(
		CGOP_SETUP_MENU_SLUG,
		__( 'GIS Boundary Import', 'county-gop-core' ),
		__( 'GIS Boundary Import', 'county-gop-core' ),
		CGOP_GIS_IMPORT_CAP,
		'cgop-gis-boundary-import',
		'CGOP_gis_render_admin_page'
	);
}
add_action( 'admin_menu', 'CGOP_gis_register_admin_page' );

/**
 * Render admin page.
 */
function CGOP_gis_render_admin_page() {
	if ( ! current_user_can( CGOP_GIS_IMPORT_CAP ) ) {
		wp_die( esc_html__( 'You do not have permission to manage GIS boundary imports.', 'county-gop-core' ) );
	}

	$settings        = CGOP_gis_get_settings();
	$layers          = CGOP_gis_get_layer_registry();
	$status          = get_option( CGOP_GIS_IMPORT_STATUS_OPTION, array() );
	$manifest_url    = CGOP_gis_get_generated_file_url( 'manifest.json' );
	$preview_payload = CGOP_gis_get_label_preview_payload();
	$preview_form    = ! empty( $preview_payload['form'] ) && is_array( $preview_payload['form'] ) ? $preview_payload['form'] : array();
	$request_memory  = CGOP_gis_get_request_memory();
	$direct_imports  = CGOP_gis_get_direct_geojson_imports();
	$composites      = CGOP_gis_get_composite_maps();
	$source_choices  = CGOP_gis_get_composite_source_choices();
	?>
	<div class="wrap cgop-gis-admin">
		<h1><?php esc_html_e( 'GIS Boundary Import / GeoJSON Generator', 'county-gop-core' ); ?></h1>
		<?php CGOP_gis_render_admin_styles(); ?>

		<?php CGOP_gis_render_notices(); ?>

		<p class="cgop-gis-intro"><?php esc_html_e( 'Regenerate local GeoJSON files from configured Beacon GIS layers, combine generated files into composites, and prepare map boundaries for Position Keys. This is an admin-only workflow; public pages should use generated static files and should not call Beacon directly.', 'county-gop-core' ); ?></p>

		<section class="cgop-gis-panel cgop-gis-panel--request">
			<div class="cgop-gis-panel__header">
				<div>
					<h2><?php esc_html_e( 'Beacon Request Settings', 'county-gop-core' ); ?></h2>
					<p><?php esc_html_e( 'Temporary request values used when regenerating Beacon layers. QPS, Cookie, User-Agent, and field overrides are remembered briefly for your admin user only.', 'county-gop-core' ); ?></p>
				</div>
			</div>
		<form id="cgop-gis-regenerate-form" method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
			<?php wp_nonce_field( 'CGOP_gis_regenerate' ); ?>
			<input type="hidden" name="action" value="CGOP_gis_regenerate">
			<table class="form-table" role="presentation">
				<tr>
					<th scope="row"><label for="cgop-gis-qps"><?php esc_html_e( 'Beacon QPS value', 'county-gop-core' ); ?></label></th>
					<td>
						<input class="regular-text" id="cgop-gis-qps" name="qps" type="text" value="<?php echo esc_attr( $request_memory['qps'] ); ?>" autocomplete="off" required>
						<p class="description"><?php esc_html_e( 'Paste the current QPS value from the Beacon URL or Network request. It is remembered for your admin user for about two hours so you can regenerate multiple layers.', 'county-gop-core' ); ?></p>
					</td>
				</tr>
				<tr>
					<th scope="row"><label for="cgop-gis-cookie"><?php esc_html_e( 'Beacon Cookie header', 'county-gop-core' ); ?></label></th>
					<td>
						<textarea class="large-text code" id="cgop-gis-cookie" name="beacon_cookie" rows="4" autocomplete="off" spellcheck="false"><?php echo esc_textarea( $request_memory['cookie'] ); ?></textarea>
						<p class="description"><?php esc_html_e( 'Optional but often required. From DevTools, copy the Cookie header or the value after curl -b. It is remembered for your admin user for about two hours and is not saved to layer definitions.', 'county-gop-core' ); ?></p>
					</td>
				</tr>
				<tr>
					<th scope="row"><label for="cgop-gis-user-agent"><?php esc_html_e( 'Browser User-Agent', 'county-gop-core' ); ?></label></th>
					<td>
						<input class="large-text" id="cgop-gis-user-agent" name="beacon_user_agent" type="text" value="<?php echo esc_attr( $request_memory['user_agent'] ); ?>" autocomplete="off">
						<p class="description"><?php esc_html_e( 'Optional browser User-Agent to send with the Beacon request. The default mimics a normal desktop browser.', 'county-gop-core' ); ?></p>
					</td>
				</tr>
				<tr>
					<th scope="row"><label for="cgop-gis-endpoint"><?php esc_html_e( 'Beacon endpoint', 'county-gop-core' ); ?></label></th>
					<td>
						<input class="large-text" id="cgop-gis-endpoint" name="beacon_endpoint" type="url" value="<?php echo esc_attr( $settings['beacon_endpoint'] ); ?>" required>
					</td>
				</tr>
				<tr>
					<th scope="row"><?php esc_html_e( 'County extent', 'county-gop-core' ); ?></th>
					<td>
						<label><?php esc_html_e( 'Min X', 'county-gop-core' ); ?> <input name="minx" type="number" step="1" value="<?php echo esc_attr( $settings['minx'] ); ?>"></label>
						<label><?php esc_html_e( 'Min Y', 'county-gop-core' ); ?> <input name="miny" type="number" step="1" value="<?php echo esc_attr( $settings['miny'] ); ?>"></label>
						<label><?php esc_html_e( 'Max X', 'county-gop-core' ); ?> <input name="maxx" type="number" step="1" value="<?php echo esc_attr( $settings['maxx'] ); ?>"></label>
						<label><?php esc_html_e( 'Max Y', 'county-gop-core' ); ?> <input name="maxy" type="number" step="1" value="<?php echo esc_attr( $settings['maxy'] ); ?>"></label>
					</td>
				</tr>
				<tr>
					<th scope="row"><label for="cgop-gis-feature-limit"><?php esc_html_e( 'Feature limit', 'county-gop-core' ); ?></label></th>
					<td><input id="cgop-gis-feature-limit" name="feature_limit" type="number" min="1" max="5000" value="<?php echo esc_attr( $settings['feature_limit'] ); ?>"></td>
				</tr>
				<tr>
					<th scope="row"><label for="cgop-gis-field-overrides"><?php esc_html_e( 'Feature field overrides', 'county-gop-core' ); ?></label></th>
					<td>
						<textarea class="large-text code" id="cgop-gis-field-overrides" name="field_overrides" rows="4" autocomplete="off" spellcheck="false"><?php echo esc_textarea( $request_memory['overrides'] ); ?></textarea>
						<p class="description"><?php esc_html_e( 'Optional. One override per line, matched by Beacon Key or DisplayKey. Example: 5: Community=Auburn, Municipal District=5. These values are remembered for your admin user for about two hours and are not saved to layer definitions.', 'county-gop-core' ); ?></p>
					</td>
				</tr>
			</table>
		</form>
		</section>

		<section class="cgop-gis-panel">
			<div class="cgop-gis-panel__header">
				<div>
					<h2><?php esc_html_e( 'Known Beacon Layers', 'county-gop-core' ); ?></h2>
					<p><?php esc_html_e( 'Saved Beacon layer definitions. Use the request settings above, then regenerate one layer or all layers from this table.', 'county-gop-core' ); ?></p>
				</div>
			</div>
		<?php if ( $layers ) : ?>
			<table class="widefat striped cgop-gis-table">
				<thead>
					<tr>
						<th><?php esc_html_e( 'Layer', 'county-gop-core' ); ?></th>
						<th><?php esc_html_e( 'Beacon ID', 'county-gop-core' ); ?></th>
						<th><?php esc_html_e( 'Output', 'county-gop-core' ); ?></th>
						<th><?php esc_html_e( 'Last Run', 'county-gop-core' ); ?></th>
						<th><?php esc_html_e( 'Features', 'county-gop-core' ); ?></th>
						<th><?php esc_html_e( 'Action', 'county-gop-core' ); ?></th>
					</tr>
				</thead>
				<tbody>
					<?php foreach ( $layers as $layer ) : ?>
						<?php $layer_status = $status['layers'][ $layer['id'] ] ?? array(); ?>
						<tr>
							<td><strong><?php echo esc_html( $layer['label'] ); ?></strong></td>
							<td><?php echo esc_html( (string) $layer['beacon_layer_id'] ); ?></td>
							<td><code><?php echo esc_html( $layer['output_dir'] ); ?></code></td>
							<td><?php echo ! empty( $layer_status['generated_at'] ) ? esc_html( $layer_status['generated_at'] ) : esc_html__( 'Never', 'county-gop-core' ); ?></td>
							<td><?php echo isset( $layer_status['feature_count'] ) ? esc_html( number_format_i18n( (int) $layer_status['feature_count'] ) ) : '0'; ?></td>
							<td>
								<div class="cgop-gis-actions">
									<button class="button" type="submit" form="cgop-gis-regenerate-form" name="layer_id" value="<?php echo esc_attr( $layer['id'] ); ?>">
										<?php esc_html_e( 'Regenerate', 'county-gop-core' ); ?>
									</button>
									<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
										<input type="hidden" name="action" value="CGOP_gis_delete_custom_layer">
										<input type="hidden" name="layer_id" value="<?php echo esc_attr( $layer['id'] ); ?>">
										<?php wp_nonce_field( 'CGOP_gis_delete_custom_layer_' . $layer['id'] ); ?>
										<button type="submit" class="button button-link-delete" onclick="return confirm('<?php echo esc_attr__( 'Delete this Beacon layer definition? Generated files already created for it will remain until deleted separately.', 'county-gop-core' ); ?>');">
											<?php esc_html_e( 'Delete layer', 'county-gop-core' ); ?>
										</button>
									</form>
								</div>
							</td>
						</tr>
					<?php endforeach; ?>
				</tbody>
			</table>

			<p class="cgop-gis-actions">
				<button class="button button-primary" type="submit" form="cgop-gis-regenerate-form" name="layer_id" value="all">
					<?php esc_html_e( 'Regenerate all layers', 'county-gop-core' ); ?>
				</button>
			</p>
		<?php else : ?>
			<p><?php esc_html_e( 'No Beacon layers are configured yet. Add a known Beacon layer below before regenerating maps.', 'county-gop-core' ); ?></p>
		<?php endif; ?>
		</section>

		<section class="cgop-gis-panel">
			<div class="cgop-gis-panel__header">
				<div>
					<h2><?php esc_html_e( 'Generated Files', 'county-gop-core' ); ?></h2>
					<p><?php esc_html_e( 'Local GeoJSON files produced by Beacon regeneration. These files can be assigned directly to positions or used as composite map sources.', 'county-gop-core' ); ?></p>
				</div>
				<a class="button" href="<?php echo esc_url( $manifest_url ); ?>" target="_blank" rel="noopener">
					<?php esc_html_e( 'Open manifest.json', 'county-gop-core' ); ?>
				</a>
			</div>
		<?php CGOP_gis_render_generated_files( $status ); ?>
		</section>

		<?php CGOP_gis_render_direct_geojson_imports_section( $direct_imports ); ?>

		<?php CGOP_gis_render_composite_maps_section( $composites, $source_choices ); ?>

		<section class="cgop-gis-panel">
			<div class="cgop-gis-panel__header">
				<div>
					<h2><?php esc_html_e( 'Add Known Beacon Layer', 'county-gop-core' ); ?></h2>
					<p><?php esc_html_e( 'Add a Beacon layer definition when you know the Beacon layer ID. The internal layer ID is generated automatically, and saved layers appear in the Known Beacon Layers table above.', 'county-gop-core' ); ?></p>
				</div>
			</div>
		<?php CGOP_gis_render_label_preview( $preview_payload ); ?>
		<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
			<?php wp_nonce_field( 'CGOP_gis_add_custom_layer' ); ?>
			<input type="hidden" name="action" value="CGOP_gis_add_custom_layer">
			<table class="form-table" role="presentation">
				<tr>
					<th scope="row"><label for="custom_layer_label"><?php esc_html_e( 'Layer label', 'county-gop-core' ); ?></label></th>
					<td><input class="regular-text" id="custom_layer_label" name="label" type="text" value="<?php echo esc_attr( $preview_form['label'] ?? '' ); ?>" required></td>
				</tr>
				<tr>
					<th scope="row"><label for="custom_layer_beacon_id"><?php esc_html_e( 'Beacon layer ID', 'county-gop-core' ); ?></label></th>
					<td><input id="custom_layer_beacon_id" name="beacon_layer_id" type="number" min="1" value="<?php echo esc_attr( $preview_form['beacon_layer_id'] ?? '' ); ?>" required></td>
				</tr>
				<tr>
					<th scope="row"><label for="custom_layer_output_dir"><?php esc_html_e( 'Output directory', 'county-gop-core' ); ?></label></th>
					<td>
						<input class="regular-text" id="custom_layer_output_dir" name="output_dir" type="text" value="<?php echo esc_attr( $preview_form['output_dir'] ?? '' ); ?>">
						<p class="description"><?php esc_html_e( 'Optional. Leave blank to generate from the layer label. The internal layer ID is generated automatically.', 'county-gop-core' ); ?></p>
					</td>
				</tr>
				<tr>
					<th scope="row"><label for="custom_layer_position_type"><?php esc_html_e( 'Position type', 'county-gop-core' ); ?></label></th>
					<td><input class="regular-text" id="custom_layer_position_type" name="position_type" type="text" value="<?php echo esc_attr( $preview_form['position_type'] ?? '' ); ?>" placeholder="county_commissioner"></td>
				</tr>
				<tr>
					<th scope="row"><label for="custom_layer_level"><?php esc_html_e( 'Level', 'county-gop-core' ); ?></label></th>
					<td><input class="regular-text" id="custom_layer_level" name="level" type="text" value="<?php echo esc_attr( $preview_form['level'] ?? '' ); ?>" placeholder="county"></td>
				</tr>
				<tr>
					<th scope="row"><label for="custom_layer_preview_qps"><?php esc_html_e( 'Preview QPS value', 'county-gop-core' ); ?></label></th>
					<td>
						<input class="regular-text" id="custom_layer_preview_qps" name="preview_qps" type="text" value="<?php echo esc_attr( $preview_form['preview_qps'] ?? $request_memory['qps'] ); ?>" autocomplete="off">
						<p class="description"><?php esc_html_e( 'Used when fetching available label fields. It is remembered for your admin user for about two hours and is not saved with the layer.', 'county-gop-core' ); ?></p>
					</td>
				</tr>
				<tr>
					<th scope="row"><label for="custom_layer_preview_cookie"><?php esc_html_e( 'Preview Cookie header', 'county-gop-core' ); ?></label></th>
					<td>
						<textarea class="large-text code" id="custom_layer_preview_cookie" name="preview_cookie" rows="3" autocomplete="off" spellcheck="false"><?php echo esc_textarea( $preview_form['preview_cookie'] ?? $request_memory['cookie'] ); ?></textarea>
						<p class="description"><?php esc_html_e( 'Optional but often needed for Beacon 403 responses. It is remembered for your admin user for about two hours and is not saved with the layer.', 'county-gop-core' ); ?></p>
					</td>
				</tr>
				<tr>
					<th scope="row"><label for="custom_layer_preview_user_agent"><?php esc_html_e( 'Preview User-Agent', 'county-gop-core' ); ?></label></th>
					<td>
						<input class="large-text" id="custom_layer_preview_user_agent" name="preview_user_agent" type="text" value="<?php echo esc_attr( $preview_form['preview_user_agent'] ?? $request_memory['user_agent'] ); ?>" autocomplete="off">
					</td>
				</tr>
				<tr>
					<th scope="row"><?php esc_html_e( 'Fetch label fields', 'county-gop-core' ); ?></th>
					<td>
						<button class="button" type="submit" name="CGOP_gis_preview_label_fields" value="1">
							<?php esc_html_e( 'Fetch Available Label Fields', 'county-gop-core' ); ?>
						</button>
						<p class="description"><?php esc_html_e( 'Fetches up to 3 features from this Beacon layer, shows available field names and sample values, and preserves the form without saving the layer.', 'county-gop-core' ); ?></p>
					</td>
				</tr>
				<tr>
					<th scope="row"><label for="custom_layer_fields"><?php esc_html_e( 'Preferred label fields', 'county-gop-core' ); ?></label></th>
					<td>
						<input class="large-text" id="custom_layer_fields" name="preferred_fields" type="text" value="<?php echo esc_attr( $preview_form['preferred_fields'] ?? 'DisplayKey' ); ?>">
						<p class="description"><?php esc_html_e( 'Comma-separated Beacon field names to combine for feature labels. Example: Community, Municipal District.', 'county-gop-core' ); ?></p>
					</td>
				</tr>
				<tr>
					<th scope="row"><label for="custom_layer_name_template"><?php esc_html_e( 'Position name template', 'county-gop-core' ); ?></label></th>
					<td><input class="large-text" id="custom_layer_name_template" name="position_name_template" type="text" value="<?php echo esc_attr( $preview_form['position_name_template'] ?? '{label}' ); ?>"></td>
				</tr>
				<tr>
					<th scope="row"><label for="custom_layer_id_template"><?php esc_html_e( 'Position ID / filename template', 'county-gop-core' ); ?></label></th>
					<td>
						<input class="large-text" id="custom_layer_id_template" name="position_id_template" type="text" value="<?php echo esc_attr( $preview_form['position_id_template'] ?? '{label_slug}' ); ?>">
						<p class="description"><?php esc_html_e( 'Supports {label}, {label_slug}, and Beacon field placeholders such as {Community}, {Community_slug}, {Municipal District}, and {Municipal District_slug}. The generated filename is this rendered value plus .geojson.', 'county-gop-core' ); ?></p>
					</td>
				</tr>
			</table>
			<?php submit_button( __( 'Add Known Layer', 'county-gop-core' ) ); ?>
		</form>
		</section>

		<?php do_action( 'CGOP_gis_admin_extra_sections' ); ?>
	</div>
	<?php
}

/**
 * Render scoped admin styles for the GIS Boundary Import page.
 */
function CGOP_gis_render_admin_styles() {
	?>
	<style>
		.cgop-gis-admin {
			max-width: 1440px;
		}

		.cgop-gis-intro {
			max-width: 980px;
			margin: 12px 0 20px;
			font-size: 14px;
			line-height: 1.55;
		}

		.cgop-gis-panel {
			margin: 18px 0;
			padding: 18px 20px 20px;
			background: #fff;
			border: 1px solid #c3c4c7;
			border-radius: 6px;
			box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
		}

		.cgop-gis-panel--request {
			border-left: 4px solid #2271b1;
		}

		.cgop-gis-panel__header {
			display: flex;
			align-items: flex-start;
			justify-content: space-between;
			gap: 16px;
			margin-bottom: 14px;
			padding-bottom: 12px;
			border-bottom: 1px solid #dcdcde;
		}

		.cgop-gis-panel__header h2,
		.cgop-gis-panel h3 {
			margin: 0;
			color: #1d2327;
		}

		.cgop-gis-panel__header p {
			max-width: 860px;
			margin: 6px 0 0;
			color: #50575e;
			line-height: 1.5;
		}

		.cgop-gis-panel .form-table {
			margin-top: 0;
		}

		.cgop-gis-panel .form-table th {
			width: 190px;
			padding-left: 0;
		}

		.cgop-gis-panel .form-table td {
			padding-right: 0;
		}

		.cgop-gis-panel input.regular-text,
		.cgop-gis-panel input.large-text,
		.cgop-gis-panel textarea.large-text {
			max-width: 860px;
		}

		.cgop-gis-panel .submit {
			margin-bottom: 0;
			padding-bottom: 0;
		}

		.cgop-gis-panel code {
			white-space: normal;
			word-break: break-word;
		}

		.cgop-gis-id {
			font-size: 11px;
		}

		.cgop-gis-required {
			color: #b32d2e;
		}

		.cgop-gis-table th,
		.cgop-gis-table td {
			vertical-align: middle;
		}

		.cgop-gis-actions {
			display: flex;
			flex-wrap: wrap;
			align-items: center;
			gap: 8px;
		}

		.cgop-gis-actions form {
			display: inline-flex;
			margin: 0;
		}

		.cgop-gis-preview {
			max-width: 1100px;
		}

		.cgop-gis-preview details {
			margin-top: 1em;
		}

		.cgop-gis-preview pre {
			max-height: 320px;
			overflow: auto;
			background: #fff;
			border: 1px solid #ccd0d4;
			padding: 12px;
		}

		.cgop-gis-source-list {
			max-height: 360px;
			overflow: auto;
			max-width: 980px;
			padding: 0 12px 12px;
			border: 1px solid #dcdcde;
			background: #f6f7f7;
			border-radius: 4px;
		}

		.cgop-gis-source-group {
			margin: 14px 0 6px;
			padding: 8px 10px;
			background: #fff;
			border-left: 4px solid #8c8f94;
			font-weight: 600;
		}

		.cgop-gis-source-item {
			display: block;
			margin: 0;
			padding: 7px 10px 7px 28px;
			background: #fff;
			border-bottom: 1px solid #f0f0f1;
		}

		.cgop-gis-source-item input {
			margin-left: -20px;
			margin-right: 6px;
		}

		.cgop-gis-source-item small {
			color: #646970;
		}

		@media (max-width: 782px) {
			.cgop-gis-panel {
				padding: 14px;
			}

			.cgop-gis-panel__header {
				display: block;
			}

			.cgop-gis-panel__header .button {
				margin-top: 10px;
			}
		}
	</style>
	<?php
}

/**
 * Render admin notices for the GIS page.
 */
function CGOP_gis_render_notices() {
	if ( isset( $_GET['CGOP_gis_success'] ) ) {
		echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'GIS boundary regeneration completed.', 'county-gop-core' ) . '</p></div>';
	}

	if ( isset( $_GET['CGOP_gis_deleted'] ) ) {
		echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Generated map file deleted.', 'county-gop-core' ) . '</p></div>';
	}

	if ( isset( $_GET['CGOP_gis_composite_created'] ) ) {
		echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Composite map created.', 'county-gop-core' ) . '</p></div>';
	}

	if ( isset( $_GET['CGOP_gis_composite_rebuilt'] ) ) {
		echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Composite map rebuilt.', 'county-gop-core' ) . '</p></div>';
	}

	if ( isset( $_GET['CGOP_gis_composite_deleted'] ) ) {
		echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Composite map deleted. Source files were not deleted. Position Key assignments that pointed to this composite may stop rendering until reassigned.', 'county-gop-core' ) . '</p></div>';
	}

	if ( isset( $_GET['CGOP_gis_direct_imported'] ) ) {
		echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'GeoJSON file imported and added to Position Key map choices.', 'county-gop-core' ) . '</p></div>';
	}

	if ( isset( $_GET['CGOP_gis_direct_deleted'] ) ) {
		echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Uploaded GeoJSON import deleted. Position Key assignments that pointed to it were cleared.', 'county-gop-core' ) . '</p></div>';
	}

	if ( isset( $_GET['CGOP_gis_error'] ) ) {
		echo '<div class="notice notice-error is-dismissible"><p>' . esc_html( sanitize_text_field( wp_unslash( $_GET['CGOP_gis_error'] ) ) ) . '</p></div>';
	}

	do_action( 'CGOP_gis_admin_notices' );
}

/**
 * Render generated file links from status.
 *
 * @param array $status Saved status.
 */
function CGOP_gis_render_generated_files( $status ) {
	if ( empty( $status['layers'] ) || ! is_array( $status['layers'] ) ) {
		echo '<p>' . esc_html__( 'No generated files have been recorded yet.', 'county-gop-core' ) . '</p>';
		return;
	}

	$rows = array();
	foreach ( $status['layers'] as $layer ) {
		if ( empty( $layer['files'] ) || ! is_array( $layer['files'] ) ) {
			continue;
		}
		foreach ( $layer['files'] as $file ) {
			$url = ! empty( $file['file'] ) ? CGOP_gis_get_generated_file_url( $file['file'] ) : '';
			if ( ! $url ) {
				continue;
			}
			$relative = ltrim( (string) $file['file'], '/' );
			$rows[]   = array(
				'layer'       => $layer,
				'file'        => $file,
				'relative'    => $relative,
				'url'         => $url,
				'label'       => $file['label'] ?? $file['position_id'] ?? basename( $relative ),
				'position_id' => $file['position_id'] ?? '',
			);
		}
	}

	if ( ! $rows ) {
		echo '<p>' . esc_html__( 'No generated files have been recorded yet.', 'county-gop-core' ) . '</p>';
		return;
	}
	?>
	<table class="widefat striped cgop-gis-table">
		<thead>
			<tr>
				<th><?php esc_html_e( 'File', 'county-gop-core' ); ?></th>
				<th><?php esc_html_e( 'Layer', 'county-gop-core' ); ?></th>
				<th><?php esc_html_e( 'Position ID', 'county-gop-core' ); ?></th>
				<th><?php esc_html_e( 'Path', 'county-gop-core' ); ?></th>
				<th><?php esc_html_e( 'Actions', 'county-gop-core' ); ?></th>
			</tr>
		</thead>
		<tbody>
			<?php foreach ( $rows as $row ) : ?>
				<tr>
					<td>
						<a href="<?php echo esc_url( $row['url'] ); ?>" target="_blank" rel="noopener">
							<strong><?php echo esc_html( $row['label'] ); ?></strong>
						</a>
					</td>
					<td><?php echo esc_html( $row['layer']['label'] ?? $row['layer']['layer_id'] ?? '' ); ?></td>
					<td><code><?php echo esc_html( $row['position_id'] ); ?></code></td>
					<td><code><?php echo esc_html( $row['relative'] ); ?></code></td>
					<td>
						<div class="cgop-gis-actions">
							<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
								<input type="hidden" name="action" value="CGOP_gis_delete_generated_file">
								<input type="hidden" name="file" value="<?php echo esc_attr( $row['relative'] ); ?>">
								<?php wp_nonce_field( 'CGOP_gis_delete_generated_file_' . $row['relative'] ); ?>
								<button type="submit" class="button button-link-delete" onclick="return confirm('<?php echo esc_attr__( 'Delete this generated GeoJSON file? Position map assignments that point to it may stop rendering.', 'county-gop-core' ); ?>');">
									<?php esc_html_e( 'Delete file', 'county-gop-core' ); ?>
								</button>
							</form>
						</div>
					</td>
				</tr>
			<?php endforeach; ?>
		</tbody>
	</table>
	<?php
}

/**
 * Return and clear the current user's label-field preview payload.
 *
 * @return array
 */
function CGOP_gis_get_label_preview_payload() {
	$uid     = get_current_user_id();
	$payload = get_transient( 'CGOP_gis_label_preview_' . $uid );
	if ( $payload ) {
		delete_transient( 'CGOP_gis_label_preview_' . $uid );
	}

	return is_array( $payload ) ? $payload : array();
}

/**
 * Render label-field preview results for the known Beacon layer form.
 *
 * @param array $payload Preview payload.
 */
function CGOP_gis_render_label_preview( $payload ) {
	if ( empty( $payload ) || ! is_array( $payload ) ) {
		return;
	}

	if ( ! empty( $payload['error'] ) ) {
		echo '<div class="notice notice-error inline"><p>' . esc_html( $payload['error'] ) . '</p></div>';
		return;
	}

	$fields  = ! empty( $payload['fields'] ) && is_array( $payload['fields'] ) ? $payload['fields'] : array();
	$records = ! empty( $payload['records'] ) && is_array( $payload['records'] ) ? $payload['records'] : array();
	if ( ! $fields ) {
		echo '<div class="notice notice-warning inline"><p>' . esc_html__( 'Beacon returned records, but no label fields could be parsed. Try DisplayKey as the preferred label field.', 'county-gop-core' ) . '</p></div>';
		return;
	}
	?>
	<div class="notice notice-info inline cgop-gis-preview">
		<p><strong><?php esc_html_e( 'Available label fields from Beacon preview', 'county-gop-core' ); ?></strong></p>
		<p><?php esc_html_e( 'Copy one or more field names into Preferred label fields. Multiple fields are combined in order, separated by commas.', 'county-gop-core' ); ?></p>
		<table class="widefat striped cgop-gis-table">
			<thead>
				<tr>
					<th><?php esc_html_e( 'Field name', 'county-gop-core' ); ?></th>
					<th><?php esc_html_e( 'Sample values', 'county-gop-core' ); ?></th>
				</tr>
			</thead>
			<tbody>
				<?php foreach ( $fields as $field_name => $values ) : ?>
					<tr>
						<td><code><?php echo esc_html( $field_name ); ?></code></td>
						<td><?php echo esc_html( implode( ' | ', array_filter( array_unique( array_map( 'strval', (array) $values ) ) ) ) ); ?></td>
					</tr>
				<?php endforeach; ?>
			</tbody>
		</table>
		<?php if ( $records ) : ?>
			<details>
				<summary><?php esc_html_e( 'Show raw preview records', 'county-gop-core' ); ?></summary>
				<pre><?php echo esc_html( wp_json_encode( $records, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); ?></pre>
			</details>
		<?php endif; ?>
	</div>
	<?php
}

/**
 * Handle regeneration POST.
 */
function CGOP_gis_handle_regenerate() {
	if ( ! current_user_can( CGOP_GIS_IMPORT_CAP ) ) {
		wp_die( esc_html__( 'You do not have permission to manage GIS boundary imports.', 'county-gop-core' ) );
	}

	check_admin_referer( 'CGOP_gis_regenerate' );

	$settings = CGOP_gis_sanitize_settings( wp_unslash( $_POST ) );
	update_option( CGOP_GIS_IMPORT_SETTINGS_OPTION, $settings, false );

	$qps = isset( $_POST['qps'] ) ? sanitize_text_field( wp_unslash( $_POST['qps'] ) ) : '';
	if ( '' === $qps ) {
		CGOP_gis_redirect_with_error( __( 'Beacon QPS value is required.', 'county-gop-core' ) );
	}
	$request_context = array(
		'cookie'          => isset( $_POST['beacon_cookie'] ) ? CGOP_gis_normalize_cookie_header( wp_unslash( $_POST['beacon_cookie'] ) ) : '',
		'user_agent'      => isset( $_POST['beacon_user_agent'] ) ? sanitize_text_field( wp_unslash( $_POST['beacon_user_agent'] ) ) : CGOP_gis_get_default_user_agent(),
		'field_overrides' => isset( $_POST['field_overrides'] ) ? CGOP_gis_parse_field_overrides( wp_unslash( $_POST['field_overrides'] ) ) : array(),
	);
	$field_overrides_raw = isset( $_POST['field_overrides'] ) ? sanitize_textarea_field( wp_unslash( $_POST['field_overrides'] ) ) : '';
	CGOP_gis_set_request_memory( $qps, $request_context['cookie'], $request_context['user_agent'], $field_overrides_raw );

	$layer_id = isset( $_POST['layer_id'] ) ? sanitize_key( wp_unslash( $_POST['layer_id'] ) ) : '';
	$layers   = CGOP_gis_get_layer_registry();

	if ( 'all' === $layer_id ) {
		$selected_layers = array_values( $layers );
	} elseif ( isset( $layers[ $layer_id ] ) ) {
		$selected_layers = array( $layers[ $layer_id ] );
	} else {
		CGOP_gis_redirect_with_error( __( 'Choose a valid GIS layer to regenerate.', 'county-gop-core' ) );
	}

	$status = get_option( CGOP_GIS_IMPORT_STATUS_OPTION, array() );
	if ( ! is_array( $status ) ) {
		$status = array();
	}
	if ( empty( $status['layers'] ) || ! is_array( $status['layers'] ) ) {
		$status['layers'] = array();
	}

	foreach ( $selected_layers as $layer ) {
		$result = CGOP_gis_regenerate_layer( $layer, $qps, $settings, $request_context );
		if ( is_wp_error( $result ) ) {
			CGOP_gis_redirect_with_error( $result->get_error_message() );
		}
		$status['layers'][ $layer['id'] ] = $result;
	}

	$manifest_result = CGOP_gis_write_manifest( $status );
	if ( is_wp_error( $manifest_result ) ) {
		CGOP_gis_redirect_with_error( $manifest_result->get_error_message() );
	}

	update_option( CGOP_GIS_IMPORT_STATUS_OPTION, $status, false );

	wp_safe_redirect( add_query_arg( 'CGOP_gis_success', '1', CGOP_gis_get_admin_url() ) );
	exit;
}
add_action( 'admin_post_CGOP_gis_regenerate', 'CGOP_gis_handle_regenerate' );

/**
 * Handle adding a known Beacon layer.
 */
function CGOP_gis_handle_add_custom_layer() {
	if ( ! current_user_can( CGOP_GIS_IMPORT_CAP ) ) {
		wp_die( esc_html__( 'You do not have permission to manage GIS boundary imports.', 'county-gop-core' ) );
	}

	check_admin_referer( 'CGOP_gis_add_custom_layer' );

	if ( ! empty( $_POST['CGOP_gis_preview_label_fields'] ) ) {
		CGOP_gis_handle_label_preview();
	}

	$layer = CGOP_gis_normalize_layer_config( wp_unslash( $_POST ) );
	if ( empty( $layer['id'] ) || empty( $layer['label'] ) || empty( $layer['beacon_layer_id'] ) ) {
		CGOP_gis_redirect_with_error( __( 'Known Beacon layer requires a label and Beacon layer ID.', 'county-gop-core' ) );
	}

	$custom = CGOP_gis_get_custom_layer_registry();
	$custom[ $layer['id'] ] = $layer;
	update_option( CGOP_GIS_IMPORT_CUSTOM_LAYERS_OPTION, $custom, false );

	wp_safe_redirect( add_query_arg( 'CGOP_gis_success', '1', CGOP_gis_get_admin_url() ) );
	exit;
}
add_action( 'admin_post_CGOP_gis_add_custom_layer', 'CGOP_gis_handle_add_custom_layer' );

/**
 * Fetch sample Beacon fields for the manual layer form without saving it.
 */
function CGOP_gis_handle_label_preview() {
	$uid        = get_current_user_id();
	$raw_form   = wp_unslash( $_POST );
	$form_state = CGOP_gis_sanitize_label_preview_form_state( $raw_form );
	$payload    = array( 'form' => $form_state );

	$layer = CGOP_gis_normalize_layer_config( $raw_form );
	if ( empty( $layer['label'] ) || empty( $layer['beacon_layer_id'] ) ) {
		$payload['error'] = __( 'Enter a layer label and Beacon layer ID before fetching label fields.', 'county-gop-core' );
		set_transient( 'CGOP_gis_label_preview_' . $uid, $payload, 120 );
		wp_safe_redirect( CGOP_gis_get_admin_url() );
		exit;
	}

	$qps = isset( $raw_form['preview_qps'] ) ? sanitize_text_field( $raw_form['preview_qps'] ) : '';
	if ( '' === $qps ) {
		$payload['error'] = __( 'Enter a Preview QPS value before fetching label fields.', 'county-gop-core' );
		set_transient( 'CGOP_gis_label_preview_' . $uid, $payload, 120 );
		wp_safe_redirect( CGOP_gis_get_admin_url() );
		exit;
	}

	$settings                  = CGOP_gis_get_settings();
	$settings['feature_limit'] = 3;
	$request_context           = array(
		'cookie'     => isset( $raw_form['preview_cookie'] ) ? CGOP_gis_normalize_cookie_header( $raw_form['preview_cookie'] ) : '',
		'user_agent' => isset( $raw_form['preview_user_agent'] ) ? sanitize_text_field( $raw_form['preview_user_agent'] ) : CGOP_gis_get_default_user_agent(),
	);
	CGOP_gis_set_request_memory( $qps, $request_context['cookie'], $request_context['user_agent'] );

	$records = CGOP_gis_fetch_beacon_layer( $layer, $qps, $settings, $request_context );
	if ( is_wp_error( $records ) ) {
		$payload['error'] = $records->get_error_message();
		set_transient( 'CGOP_gis_label_preview_' . $uid, $payload, 120 );
		wp_safe_redirect( CGOP_gis_get_admin_url() );
		exit;
	}

	$preview_records = array_slice( (array) $records, 0, 3 );
	$payload['fields']  = CGOP_gis_collect_preview_label_fields( $preview_records );
	$payload['records'] = CGOP_gis_trim_preview_records( $preview_records );

	set_transient( 'CGOP_gis_label_preview_' . $uid, $payload, 120 );
	wp_safe_redirect( CGOP_gis_get_admin_url() );
	exit;
}

/**
 * Sanitize manual-layer form state for redisplay after preview.
 *
 * @param array $raw Raw form values.
 * @return array
 */
function CGOP_gis_sanitize_label_preview_form_state( array $raw ) {
	return array(
		'label'                  => sanitize_text_field( $raw['label'] ?? '' ),
		'beacon_layer_id'        => absint( $raw['beacon_layer_id'] ?? 0 ),
		'output_dir'             => sanitize_title( $raw['output_dir'] ?? '' ),
		'position_type'          => sanitize_key( $raw['position_type'] ?? '' ),
		'level'                  => sanitize_key( $raw['level'] ?? '' ),
		'preview_qps'            => sanitize_text_field( $raw['preview_qps'] ?? '' ),
		'preview_cookie'         => isset( $raw['preview_cookie'] ) ? CGOP_gis_normalize_cookie_header( $raw['preview_cookie'] ) : '',
		'preview_user_agent'     => sanitize_text_field( $raw['preview_user_agent'] ?? CGOP_gis_get_default_user_agent() ),
		'preferred_fields'       => sanitize_text_field( $raw['preferred_fields'] ?? 'DisplayKey' ),
		'position_name_template' => sanitize_text_field( $raw['position_name_template'] ?? '{label}' ),
		'position_id_template'   => sanitize_text_field( $raw['position_id_template'] ?? '{label_slug}' ),
	);
}

/**
 * Collect label field names and sample values from Beacon records.
 *
 * @param array $records Beacon records.
 * @return array
 */
function CGOP_gis_collect_preview_label_fields( array $records ) {
	$fields = array();

	foreach ( $records as $record ) {
		if ( ! is_array( $record ) ) {
			continue;
		}

		foreach ( array( 'Key', 'DisplayKey' ) as $direct_field ) {
			if ( isset( $record[ $direct_field ] ) && '' !== trim( (string) $record[ $direct_field ] ) ) {
				$fields[ $direct_field ][] = trim( (string) $record[ $direct_field ] );
			}
		}

		$parsed = CGOP_gis_parse_html_fields( (string) ( $record['ResultHtml'] ?? '' ) )
			+ CGOP_gis_parse_html_fields( (string) ( $record['TipHtml'] ?? '' ) );
		foreach ( $parsed as $field_name => $value ) {
			if ( '' === trim( (string) $value ) || '#MISSING#' === $value ) {
				continue;
			}
			$fields[ $field_name ][] = trim( (string) $value );
		}
	}

	ksort( $fields, SORT_NATURAL | SORT_FLAG_CASE );

	return $fields;
}

/**
 * Trim Beacon records to preview-friendly fields.
 *
 * @param array $records Beacon records.
 * @return array
 */
function CGOP_gis_trim_preview_records( array $records ) {
	$trimmed = array();

	foreach ( $records as $record ) {
		if ( ! is_array( $record ) ) {
			continue;
		}
		$trimmed[] = array(
			'Key'        => $record['Key'] ?? '',
			'DisplayKey' => $record['DisplayKey'] ?? '',
			'ResultHtml' => $record['ResultHtml'] ?? '',
			'TipHtml'    => $record['TipHtml'] ?? '',
		);
	}

	return $trimmed;
}

/**
 * Handle deleting a known Beacon layer.
 */
function CGOP_gis_handle_delete_custom_layer() {
	if ( ! current_user_can( CGOP_GIS_IMPORT_CAP ) ) {
		wp_die( esc_html__( 'You do not have permission to manage GIS boundary imports.', 'county-gop-core' ) );
	}

	$layer_id = isset( $_POST['layer_id'] ) ? sanitize_key( wp_unslash( $_POST['layer_id'] ) ) : '';
	check_admin_referer( 'CGOP_gis_delete_custom_layer_' . $layer_id );
	if ( '' === $layer_id ) {
		CGOP_gis_redirect_with_error( __( 'Choose a valid Beacon layer to delete.', 'county-gop-core' ) );
	}

	$custom = CGOP_gis_get_custom_layer_registry();
	if ( $layer_id && isset( $custom[ $layer_id ] ) ) {
		unset( $custom[ $layer_id ] );
		update_option( CGOP_GIS_IMPORT_CUSTOM_LAYERS_OPTION, $custom, false );
	}

	wp_safe_redirect( add_query_arg( 'CGOP_gis_success', '1', CGOP_gis_get_admin_url() ) );
	exit;
}
add_action( 'admin_post_CGOP_gis_delete_custom_layer', 'CGOP_gis_handle_delete_custom_layer' );

/**
 * Handle deleting a generated GeoJSON file.
 */
function CGOP_gis_handle_delete_generated_file() {
	if ( ! current_user_can( CGOP_GIS_IMPORT_CAP ) ) {
		wp_die( esc_html__( 'You do not have permission to manage GIS boundary imports.', 'county-gop-core' ) );
	}

	$relative = isset( $_POST['file'] ) ? ltrim( sanitize_text_field( wp_unslash( $_POST['file'] ) ), '/' ) : '';
	check_admin_referer( 'CGOP_gis_delete_generated_file_' . $relative );

	if ( '' === $relative || str_contains( $relative, '..' ) || ! preg_match( '/\.(geojson|json)$/i', $relative ) ) {
		CGOP_gis_redirect_with_error( __( 'Invalid generated file path.', 'county-gop-core' ) );
	}

	$base      = wp_normalize_path( trailingslashit( CGOP_gis_get_generated_dir() ) );
	$file_path = wp_normalize_path( $base . $relative );
	if ( ! str_starts_with( $file_path, $base ) ) {
		CGOP_gis_redirect_with_error( __( 'Generated file path is outside the allowed map directory.', 'county-gop-core' ) );
	}

	if ( file_exists( $file_path ) && ! wp_delete_file( $file_path ) ) {
		CGOP_gis_redirect_with_error( __( 'Could not delete the generated GeoJSON file.', 'county-gop-core' ) );
	}

	$status = get_option( CGOP_GIS_IMPORT_STATUS_OPTION, array() );
	if ( ! is_array( $status ) ) {
		$status = array();
	}

	if ( ! empty( $status['layers'] ) && is_array( $status['layers'] ) ) {
		foreach ( $status['layers'] as $layer_id => $layer_status ) {
			if ( empty( $layer_status['files'] ) || ! is_array( $layer_status['files'] ) ) {
				continue;
			}

			$files = array();
			foreach ( $layer_status['files'] as $file ) {
				if ( empty( $file['file'] ) || ltrim( (string) $file['file'], '/' ) !== $relative ) {
					$files[] = $file;
				}
			}

			$status['layers'][ $layer_id ]['files']         = $files;
			$status['layers'][ $layer_id ]['feature_count'] = count( $files );
		}
	}

	update_option( CGOP_GIS_IMPORT_STATUS_OPTION, $status, false );
	$manifest_result = CGOP_gis_write_manifest( $status );
	if ( is_wp_error( $manifest_result ) ) {
		CGOP_gis_redirect_with_error( $manifest_result->get_error_message() );
	}

	wp_safe_redirect( add_query_arg( 'CGOP_gis_deleted', '1', CGOP_gis_get_admin_url() ) );
	exit;
}
add_action( 'admin_post_CGOP_gis_delete_generated_file', 'CGOP_gis_handle_delete_generated_file' );

/**
 * Sanitize request settings.
 *
 * @param array $raw Raw request values.
 * @return array
 */
function CGOP_gis_sanitize_settings( $raw ) {
	$defaults = CGOP_gis_get_default_settings();

	return array(
		'beacon_endpoint' => ! empty( $raw['beacon_endpoint'] ) ? esc_url_raw( $raw['beacon_endpoint'] ) : $defaults['beacon_endpoint'],
		'minx'            => isset( $raw['minx'] ) ? (float) $raw['minx'] : $defaults['minx'],
		'miny'            => isset( $raw['miny'] ) ? (float) $raw['miny'] : $defaults['miny'],
		'maxx'            => isset( $raw['maxx'] ) ? (float) $raw['maxx'] : $defaults['maxx'],
		'maxy'            => isset( $raw['maxy'] ) ? (float) $raw['maxy'] : $defaults['maxy'],
		'feature_limit'   => isset( $raw['feature_limit'] ) ? max( 1, min( 5000, absint( $raw['feature_limit'] ) ) ) : $defaults['feature_limit'],
	);
}

/**
 * Normalize a pasted Beacon Cookie header or Windows "Copy as cURL" cookie.
 *
 * Chrome-on-Windows cURL output escapes special characters with ^. Those escape
 * characters are not part of the cookie value and will break Beacon/Cloudflare
 * session matching if pasted directly.
 *
 * @param string $raw Raw pasted cookie text.
 * @return string
 */
function CGOP_gis_normalize_cookie_header( $raw ) {
	$cookie = trim( (string) $raw );

	if ( '' === $cookie ) {
		return '';
	}

	if ( preg_match( '/(?:^|\s)-b\s+\^?"([^"]+)\^?"/', $cookie, $match ) ) {
		$cookie = $match[1];
	} elseif ( preg_match( '/(?:^|\s)-b\s+\'([^\']+)\'/', $cookie, $match ) ) {
		$cookie = $match[1];
	}

	$cookie = trim( $cookie );
	$cookie = trim( $cookie, "\"' \t\n\r\0\x0B" );
	$cookie = str_replace(
		array( '^$', '^&', '^=', '^;', '^"', '^^' ),
		array( '$', '&', '=', ';', '"', '^' ),
		$cookie
	);
	$cookie = rtrim( $cookie, '^' );

	return sanitize_textarea_field( $cookie );
}

/**
 * Regenerate a single layer.
 *
 * @param array  $layer Layer config.
 * @param string $qps Beacon QPS.
 * @param array  $settings Request settings.
 * @return array|WP_Error
 */
function CGOP_gis_regenerate_layer( $layer, $qps, $settings, $request_context = array() ) {
	$records = CGOP_gis_fetch_beacon_layer( $layer, $qps, $settings, $request_context );
	if ( is_wp_error( $records ) ) {
		return $records;
	}

	if ( empty( $records ) ) {
		return new WP_Error( 'CGOP_gis_no_features', sprintf(
			/* translators: %s: layer label. */
			__( 'Beacon returned zero features for %s.', 'county-gop-core' ),
			$layer['label']
		) );
	}

	$files       = array();
	$seen_ids    = array();
	$seen_paths  = array();
	$output_base = CGOP_gis_get_generated_dir();
	$layer_dir   = trailingslashit( $output_base ) . $layer['output_dir'];

	if ( ! wp_mkdir_p( $layer_dir ) ) {
		return new WP_Error( 'CGOP_gis_mkdir_failed', __( 'Could not create the generated GeoJSON output directory.', 'county-gop-core' ) );
	}

	foreach ( $records as $record ) {
		if ( empty( $record['WktGeometry'] ) ) {
			return new WP_Error( 'CGOP_gis_missing_wkt', __( 'Beacon returned a feature without WktGeometry.', 'county-gop-core' ) );
		}

		$record_fields = CGOP_gis_get_record_fields( $record );
		$record_fields = CGOP_gis_apply_field_overrides(
			$record_fields,
			$record,
			$request_context['field_overrides'] ?? array()
		);
		$label         = CGOP_gis_extract_feature_label( $layer, $record, $record_fields );
		$label_slug    = CGOP_gis_slug( $label );
		$position_id   = CGOP_gis_apply_template( $layer['position_id_template'], $label, $label_slug, $record_fields );
		$name          = CGOP_gis_apply_template( $layer['position_name_template'], $label, $label_slug, $record_fields );
		$template_error = CGOP_gis_validate_rendered_templates( $position_id, $name, $record );
		if ( is_wp_error( $template_error ) ) {
			return $template_error;
		}
		$file_name     = $position_id . '.geojson';
		$file_path     = trailingslashit( $layer_dir ) . $file_name;
		$relative      = $layer['output_dir'] . '/' . $file_name;

		if ( isset( $seen_ids[ $position_id ] ) ) {
			return new WP_Error( 'CGOP_gis_duplicate_id', sprintf(
				/* translators: %s: generated position ID. */
				__( 'Duplicate generated position ID: %s.', 'county-gop-core' ),
				$position_id
			) );
		}

		if ( isset( $seen_paths[ $relative ] ) ) {
			return new WP_Error( 'CGOP_gis_duplicate_path', sprintf(
				/* translators: %s: generated file path. */
				__( 'Duplicate generated file path: %s.', 'county-gop-core' ),
				$relative
			) );
		}

		$geometry = CGOP_gis_convert_wkt_to_geometry( $record['WktGeometry'] );
		if ( is_wp_error( $geometry ) ) {
			return $geometry;
		}

		$feature_collection = array(
			'type'     => 'FeatureCollection',
			'features' => array(
				array(
					'type'       => 'Feature',
					'properties' => array(
						'position_id'        => $position_id,
						'source_layer_id'    => (int) $layer['beacon_layer_id'],
						'source_layer_name'  => $layer['label'],
						'source_key'         => (string) ( $record['Key'] ?? '' ),
						'source_display_key' => (string) ( $record['DisplayKey'] ?? '' ),
						'position_type'      => $layer['position_type'],
						'level'              => $layer['level'],
						'label'              => $label,
						'name'               => $name,
						'raw_result_html'    => (string) ( $record['ResultHtml'] ?? '' ),
						'raw_tip_html'       => (string) ( $record['TipHtml'] ?? '' ),
					),
					'geometry'   => $geometry,
				),
			),
		);

		$json = wp_json_encode( $feature_collection, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
		if ( ! $json || false === file_put_contents( $file_path, $json ) ) {
			return new WP_Error( 'CGOP_gis_write_failed', sprintf(
				/* translators: %s: generated file path. */
				__( 'Could not write generated GeoJSON file: %s.', 'county-gop-core' ),
				$relative
			) );
		}

		$seen_ids[ $position_id ]   = true;
		$seen_paths[ $relative ]    = true;
		$files[] = array(
			'position_id' => $position_id,
			'label'       => $label,
			'source_key'  => (string) ( $record['Key'] ?? '' ),
			'file'        => $relative,
		);
	}

	return array(
		'layer_id'        => $layer['id'],
		'label'           => $layer['label'],
		'beacon_layer_id' => (int) $layer['beacon_layer_id'],
		'generated_at'    => current_time( 'mysql' ),
		'feature_count'   => count( $files ),
		'files'           => $files,
	);
}

/**
 * Fetch a Beacon layer.
 *
 * @param array  $layer Layer config.
 * @param string $qps Beacon QPS.
 * @param array  $settings Request settings.
 * @return array|WP_Error
 */
function CGOP_gis_fetch_beacon_layer( $layer, $qps, $settings, $request_context = array() ) {
	$endpoint = add_query_arg( 'QPS', $qps, $settings['beacon_endpoint'] );
	$body     = array(
		'layerId'         => (int) $layer['beacon_layer_id'],
		'useSelection'    => false,
		'ext'             => array(
			'minx' => (float) $settings['minx'],
			'miny' => (float) $settings['miny'],
			'maxx' => (float) $settings['maxx'],
			'maxy' => (float) $settings['maxy'],
		),
		'featureLimit'    => (int) $settings['feature_limit'],
		'spatialRelation' => 1,
		'wkt'             => null,
	);

	$headers = array(
		'Accept'           => 'text/plain, */*; q=0.01',
		'Content-Type'     => 'application/json',
		'Origin'           => 'https://beacon.schneidercorp.com',
		'Referer'          => 'https://beacon.schneidercorp.com/Application.aspx?AppID=385&LayerID=6053&PageTypeID=1&PageID=3291',
		'X-Requested-With' => 'XMLHttpRequest',
		'User-Agent'       => ! empty( $request_context['user_agent'] ) ? $request_context['user_agent'] : CGOP_gis_get_default_user_agent(),
	);

	if ( ! empty( $request_context['cookie'] ) ) {
		$headers['Cookie'] = $request_context['cookie'];
	}

	$response = wp_remote_post(
		$endpoint,
		array(
			'timeout' => 60,
			'headers' => $headers,
			'body'    => wp_json_encode( $body ),
		)
	);

	if ( is_wp_error( $response ) ) {
		return $response;
	}

	$code = wp_remote_retrieve_response_code( $response );
	if ( $code < 200 || $code >= 300 ) {
		$body_snippet = wp_strip_all_tags( substr( wp_remote_retrieve_body( $response ), 0, 240 ) );
		return new WP_Error( 'CGOP_gis_beacon_http', sprintf(
			/* translators: 1: HTTP status code. 2: response body snippet. */
			__( 'Beacon request failed with HTTP %1$d. Response: %2$s', 'county-gop-core' ),
			(int) $code,
			$body_snippet ? $body_snippet : __( 'No response body.', 'county-gop-core' )
		) );
	}

	$data = json_decode( wp_remote_retrieve_body( $response ), true );
	if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $data ) ) {
		return new WP_Error( 'CGOP_gis_bad_json', __( 'Beacon returned a non-JSON response.', 'county-gop-core' ) );
	}

	if ( ! isset( $data['d'] ) || ! is_array( $data['d'] ) ) {
		return new WP_Error( 'CGOP_gis_missing_d', __( 'Beacon response did not include the expected d array.', 'county-gop-core' ) );
	}

	return $data['d'];
}

/**
 * Convert WKT to GeoJSON geometry using the bundled Python tool.
 *
 * @param string $wkt Raw WKT.
 * @return array|WP_Error
 */
function CGOP_gis_convert_wkt_to_geometry( $wkt ) {
	if ( ! function_exists( 'proc_open' ) ) {
		return new WP_Error( 'CGOP_gis_proc_open_missing', __( 'The server cannot run the Python WKT converter because proc_open is unavailable.', 'county-gop-core' ) );
	}

	$script = CGOP_CORE_PLUGIN_DIR . 'tools/gis/convert_beacon_wkt_to_geojson.py';
	if ( ! file_exists( $script ) ) {
		return new WP_Error( 'CGOP_gis_converter_missing', __( 'The Python WKT converter script is missing.', 'county-gop-core' ) );
	}

	$python = CGOP_gis_find_python_binary();
	if ( ! $python ) {
		return new WP_Error( 'CGOP_gis_python_missing', __( 'Python was not found on the server. Install Python with shapely and pyproj to regenerate Beacon GeoJSON.', 'county-gop-core' ) );
	}

	$descriptors = array(
		0 => array( 'pipe', 'r' ),
		1 => array( 'pipe', 'w' ),
		2 => array( 'pipe', 'w' ),
	);
	$command     = escapeshellarg( $python ) . ' ' . escapeshellarg( $script );
	$process     = proc_open( $command, $descriptors, $pipes );

	if ( ! is_resource( $process ) ) {
		return new WP_Error( 'CGOP_gis_converter_start_failed', __( 'Could not start the Python WKT converter.', 'county-gop-core' ) );
	}

	fwrite( $pipes[0], wp_json_encode( array( 'wkt' => $wkt ) ) );
	fclose( $pipes[0] );
	$output = stream_get_contents( $pipes[1] );
	$error  = stream_get_contents( $pipes[2] );
	fclose( $pipes[1] );
	fclose( $pipes[2] );
	$exit_code = proc_close( $process );

	$data = json_decode( $output, true );
	if ( 0 !== $exit_code || JSON_ERROR_NONE !== json_last_error() || empty( $data['ok'] ) || empty( $data['geometry'] ) ) {
		$message = ! empty( $data['error'] ) ? $data['error'] : trim( $error );
		return new WP_Error( 'CGOP_gis_converter_failed', sprintf(
			/* translators: %s: converter error. */
			__( 'WKT conversion failed: %s', 'county-gop-core' ),
			$message ? $message : __( 'Unknown converter error.', 'county-gop-core' )
		) );
	}

	return $data['geometry'];
}

/**
 * Find an available Python executable.
 *
 * @return string
 */
function CGOP_gis_find_python_binary() {
	static $found = null;

	if ( null !== $found ) {
		return $found;
	}

	$candidates = array( 'python3', 'python' );
	foreach ( $candidates as $candidate ) {
		$descriptors = array(
			0 => array( 'pipe', 'r' ),
			1 => array( 'pipe', 'w' ),
			2 => array( 'pipe', 'w' ),
		);
		$process = proc_open( escapeshellarg( $candidate ) . ' --version', $descriptors, $pipes );
		if ( ! is_resource( $process ) ) {
			continue;
		}
		fclose( $pipes[0] );
		stream_get_contents( $pipes[1] );
		stream_get_contents( $pipes[2] );
		fclose( $pipes[1] );
		fclose( $pipes[2] );
		if ( 0 === proc_close( $process ) ) {
			$found = $candidate;
			return $found;
		}
	}

	$found = '';
	return $found;
}

/**
 * Extract best label from Beacon result fields.
 *
 * @param array $layer Layer config.
 * @param array $record Beacon record.
 * @return string
 */
function CGOP_gis_extract_feature_label( $layer, $record, $fields = null ) {
	if ( ! is_array( $fields ) ) {
		$fields = CGOP_gis_get_record_fields( $record );
	}

	$values = array();
	foreach ( $layer['preferred_fields'] as $field_name ) {
		$value = CGOP_gis_get_field_value( $fields, $field_name );
		if ( '' !== $value && '#MISSING#' !== $value ) {
			$values[] = $value;
		}
	}

	if ( $values ) {
		return implode( ', ', array_unique( $values ) );
	}

	return ! empty( $record['DisplayKey'] ) ? trim( (string) $record['DisplayKey'] ) : trim( (string) ( $record['Key'] ?? 'feature' ) );
}

/**
 * Get parsed Beacon record fields with synthetic Key and DisplayKey values.
 *
 * @param array $record Beacon record.
 * @return array
 */
function CGOP_gis_get_record_fields( $record ) {
	$fields = CGOP_gis_parse_html_fields( (string) ( $record['ResultHtml'] ?? '' ) )
		+ CGOP_gis_parse_html_fields( (string) ( $record['TipHtml'] ?? '' ) );

	if ( isset( $record['Key'] ) ) {
		$fields['Key'] = trim( (string) $record['Key'] );
	}
	if ( isset( $record['DisplayKey'] ) ) {
		$fields['DisplayKey'] = trim( (string) $record['DisplayKey'] );
	}

	return $fields;
}

/**
 * Get a Beacon field value by exact or normalized field name.
 *
 * @param array  $fields     Parsed fields.
 * @param string $field_name Requested field name.
 * @return string
 */
function CGOP_gis_get_field_value( array $fields, $field_name ) {
	$field_name = trim( (string) $field_name );
	if ( '' === $field_name ) {
		return '';
	}

	if ( isset( $fields[ $field_name ] ) ) {
		return trim( (string) $fields[ $field_name ] );
	}

	$wanted = CGOP_gis_template_token( $field_name );
	foreach ( $fields as $key => $value ) {
		if ( CGOP_gis_template_token( $key ) === $wanted ) {
			return trim( (string) $value );
		}
	}

	return '';
}

/**
 * Parse temporary feature field overrides.
 *
 * Syntax:
 * 5: Community=Auburn, Municipal District=5
 *
 * @param string $raw Raw textarea value.
 * @return array
 */
function CGOP_gis_parse_field_overrides( $raw ) {
	$overrides = array();
	$lines     = preg_split( '/\r\n|\r|\n/', (string) $raw );

	foreach ( $lines as $line ) {
		$line = trim( $line );
		if ( '' === $line || 0 === strpos( $line, '#' ) ) {
			continue;
		}

		$parts = explode( ':', $line, 2 );
		if ( 2 !== count( $parts ) ) {
			continue;
		}

		$key   = trim( $parts[0] );
		$pairs = preg_split( '/,(?=(?:[^"]*"[^"]*")*[^"]*$)/', $parts[1] );
		if ( '' === $key || ! $pairs ) {
			continue;
		}

		foreach ( $pairs as $pair ) {
			$pair_parts = explode( '=', $pair, 2 );
			if ( 2 !== count( $pair_parts ) ) {
				continue;
			}

			$field = trim( $pair_parts[0] );
			$value = trim( $pair_parts[1] );
			$value = trim( $value, "\"'" );
			if ( '' === $field || '' === $value ) {
				continue;
			}

			$overrides[ $key ][ $field ] = $value;
		}
	}

	return $overrides;
}

/**
 * Apply temporary field overrides to a Beacon record.
 *
 * @param array $fields    Parsed fields.
 * @param array $record    Beacon record.
 * @param array $overrides Parsed overrides.
 * @return array
 */
function CGOP_gis_apply_field_overrides( array $fields, array $record, array $overrides ) {
	if ( ! $overrides ) {
		return $fields;
	}

	$record_keys = array_filter(
		array_map(
			'strval',
			array(
				$record['Key'] ?? '',
				$record['DisplayKey'] ?? '',
			)
		)
	);

	foreach ( $record_keys as $record_key ) {
		if ( empty( $overrides[ $record_key ] ) || ! is_array( $overrides[ $record_key ] ) ) {
			continue;
		}

		foreach ( $overrides[ $record_key ] as $field => $value ) {
			$fields[ $field ] = $value;
		}
	}

	return $fields;
}

/**
 * Parse simple Beacon <b>Field:</b>&nbsp;Value<br> HTML into key/value pairs.
 *
 * @param string $html HTML.
 * @return array
 */
function CGOP_gis_parse_html_fields( $html ) {
	$fields = array();
	if ( '' === $html ) {
		return $fields;
	}

	if ( preg_match_all( '/<b>\s*([^:<]+):\s*<\/b>\s*(?:&nbsp;|\s)*([^<]*)/i', $html, $matches, PREG_SET_ORDER ) ) {
		foreach ( $matches as $match ) {
			$key = trim( wp_strip_all_tags( html_entity_decode( $match[1], ENT_QUOTES ) ) );
			$val = trim( wp_strip_all_tags( html_entity_decode( $match[2], ENT_QUOTES ) ) );
			if ( $key ) {
				$fields[ $key ] = $val;
			}
		}
	}

	return $fields;
}

/**
 * Apply a label template.
 *
 * @param string $template Template.
 * @param string $label Label.
 * @param string $label_slug Label slug.
 * @return string
 */
function CGOP_gis_apply_template( $template, $label, $label_slug, $fields = array() ) {
	$replacements = array(
		'{label}'      => $label,
		'{label_slug}' => $label_slug,
	);

	foreach ( $fields as $field_name => $value ) {
		$value = trim( (string) $value );
		if ( '' === $value ) {
			continue;
		}

		$token = CGOP_gis_template_token( $field_name );
		if ( '' === $token ) {
			continue;
		}

		$replacements[ '{' . $field_name . '}' ]          = $value;
		$replacements[ '{' . $field_name . '_slug}' ]     = CGOP_gis_slug( $value );
		$replacements[ '{' . $token . '}' ]               = $value;
		$replacements[ '{' . $token . '_slug}' ]          = CGOP_gis_slug( $value );
		$replacements[ '{' . strtolower( $token ) . '}' ] = $value;
		$replacements[ '{' . strtolower( $token ) . '_slug}' ] = CGOP_gis_slug( $value );
	}

	return strtr( $template, $replacements );
}

/**
 * Validate rendered template output before writing generated files.
 *
 * @param string $position_id Rendered position ID.
 * @param string $name        Rendered position name.
 * @param array  $record      Beacon record.
 * @return true|WP_Error
 */
function CGOP_gis_validate_rendered_templates( $position_id, $name, $record ) {
	$combined = $position_id . ' ' . $name;
	if ( ! preg_match_all( '/\{([^}]+)\}/', $combined, $matches ) ) {
		return true;
	}

	$missing = array_values( array_unique( $matches[1] ) );
	$key     = ! empty( $record['DisplayKey'] ) ? (string) $record['DisplayKey'] : (string) ( $record['Key'] ?? '' );
	$prefix  = $key ? sprintf(
		/* translators: %s: Beacon feature key. */
		__( 'Beacon feature %s could not be generated.', 'county-gop-core' ),
		$key
	) : __( 'A Beacon feature could not be generated.', 'county-gop-core' );

	return new WP_Error(
		'CGOP_gis_unresolved_template',
		sprintf(
			/* translators: 1: feature message. 2: comma-separated placeholders. */
			__( '%1$s Missing template field(s): %2$s. Check the layer preview and update Preferred label fields or templates.', 'county-gop-core' ),
			$prefix,
			implode( ', ', $missing )
		)
	);
}

/**
 * Convert a Beacon field name into a template-safe token.
 *
 * @param string $value Field name.
 * @return string
 */
function CGOP_gis_template_token( $value ) {
	$value = html_entity_decode( wp_strip_all_tags( (string) $value ), ENT_QUOTES );
	$value = preg_replace( '/[^A-Za-z0-9]+/', '_', $value );
	$value = trim( $value, '_' );

	return $value;
}

/**
 * Slugify Beacon labels.
 *
 * @param string $value Raw value.
 * @return string
 */
function CGOP_gis_slug( $value ) {
	$value = html_entity_decode( wp_strip_all_tags( (string) $value ), ENT_QUOTES );
	$value = strtolower( $value );
	$value = preg_replace( '/[^a-z0-9]+/', '-', $value );
	$value = preg_replace( '/-+/', '-', $value );
	$value = trim( $value, '-' );

	return $value ? $value : 'feature';
}

/**
 * Write manifest.json.
 *
 * @param array $status Status data.
 * @return true|WP_Error
 */
function CGOP_gis_write_manifest( $status ) {
	$composites = CGOP_gis_get_composite_maps();
	$direct     = CGOP_gis_get_direct_geojson_imports();
	$manifest   = array(
		'generated_at' => gmdate( 'c' ),
		'source'       => 'Beacon / SchneiderCorp',
		'source_crs'   => 'EPSG:2965',
		'target_crs'   => 'EPSG:4326',
		'layers'       => array_values( $status['layers'] ?? array() ),
		'direct'       => array_values( $direct ),
		'composites'   => array_values( $composites ),
	);

	$path = trailingslashit( CGOP_gis_get_generated_dir() ) . 'manifest.json';
	if ( ! wp_mkdir_p( dirname( $path ) ) ) {
		return new WP_Error( 'CGOP_gis_manifest_dir_failed', __( 'Could not create the generated GeoJSON directory.', 'county-gop-core' ) );
	}

	$json = wp_json_encode( $manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
	if ( ! $json || false === file_put_contents( $path, $json ) ) {
		return new WP_Error( 'CGOP_gis_manifest_write_failed', __( 'Could not write generated manifest.json.', 'county-gop-core' ) );
	}

	return true;
}

/**
 * Return the generated-files base directory for the current county.
 *
 * Path: {uploads}/county-gop-maps/{county_slug}/generated
 *
 * @return string Absolute filesystem path (no trailing slash).
 */
function CGOP_gis_get_generated_dir() {
	$uploads     = wp_get_upload_dir();
	$county_id   = function_exists( 'cgop_get_current_county_id' ) ? cgop_get_current_county_id() : null;
	$county_slug = '';
	if ( $county_id ) {
		$county_slug = function_exists( 'cgop_get_county_slug' ) ? cgop_get_county_slug( $county_id ) : '';
	}

	if ( $county_slug ) {
		return trailingslashit( $uploads['basedir'] ) . 'county-gop-maps/' . $county_slug . '/generated';
	}

	return trailingslashit( $uploads['basedir'] ) . 'county-gop-maps/generated';
}

/**
 * Return a public URL for a generated file given its path relative to the
 * generated directory.
 *
 * Uses the same county-slug logic as CGOP_gis_get_generated_dir().
 *
 * @param string $relative Relative path within the generated directory.
 * @return string Full URL.
 */
function CGOP_gis_get_generated_file_url( $relative ) {
	$uploads     = wp_get_upload_dir();
	$county_id   = function_exists( 'cgop_get_current_county_id' ) ? cgop_get_current_county_id() : null;
	$county_slug = '';
	if ( $county_id ) {
		$county_slug = function_exists( 'cgop_get_county_slug' ) ? cgop_get_county_slug( $county_id ) : '';
	}

	if ( $county_slug ) {
		return trailingslashit( $uploads['baseurl'] ) . 'county-gop-maps/' . $county_slug . '/generated/' . ltrim( $relative, '/' );
	}

	return trailingslashit( $uploads['baseurl'] ) . 'county-gop-maps/generated/' . ltrim( $relative, '/' );
}

/**
 * Admin page URL.
 *
 * @return string
 */
function CGOP_gis_get_admin_url() {
	return admin_url( 'admin.php?page=cgop-gis-boundary-import' );
}

/**
 * Redirect with error.
 *
 * @param string $message Error message.
 */
function CGOP_gis_redirect_with_error( $message ) {
	wp_safe_redirect( add_query_arg( 'CGOP_gis_error', rawurlencode( $message ), CGOP_gis_get_admin_url() ) );
	exit;
}

/**
 * Render the direct GeoJSON import section.
 *
 * @param array[] $imports Current direct uploaded GeoJSON imports.
 */
function CGOP_gis_render_direct_geojson_imports_section( $imports ) {
	?>
	<section class="cgop-gis-panel" id="cgop-direct-geojson-imports">
		<div class="cgop-gis-panel__header">
			<div>
				<h2><?php esc_html_e( 'Upload GeoJSON Map', 'county-gop-core' ); ?></h2>
				<p><?php esc_html_e( 'Upload a ready-made .geojson or .json boundary file and make it available in Position Keys map dropdowns. This is for files you already have and does not call Beacon or ArcGIS.', 'county-gop-core' ); ?></p>
			</div>
		</div>

		<?php if ( $imports ) : ?>
			<table class="widefat striped cgop-gis-table">
				<thead>
					<tr>
						<th><?php esc_html_e( 'Label', 'county-gop-core' ); ?></th>
						<th><?php esc_html_e( 'Output File', 'county-gop-core' ); ?></th>
						<th><?php esc_html_e( 'Imported', 'county-gop-core' ); ?></th>
						<th><?php esc_html_e( 'Actions', 'county-gop-core' ); ?></th>
					</tr>
				</thead>
				<tbody>
					<?php foreach ( $imports as $import_id => $import ) : ?>
						<?php
						$relative = ltrim( (string) ( $import['output_file'] ?? '' ), '/' );
						$url      = $relative ? CGOP_gis_get_generated_file_url( $relative ) : '';
						?>
						<tr>
							<td><strong><?php echo esc_html( $import['label'] ?? '' ); ?></strong></td>
							<td>
								<code><?php echo esc_html( $relative ); ?></code>
								<?php if ( $url ) : ?>
									<a href="<?php echo esc_url( $url ); ?>" target="_blank" rel="noopener noreferrer" style="font-size:.85em"> <?php esc_html_e( 'View', 'county-gop-core' ); ?></a>
								<?php endif; ?>
							</td>
							<td><?php echo esc_html( $import['created_at'] ?? '' ); ?></td>
							<td>
								<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
									<input type="hidden" name="action" value="CGOP_gis_delete_direct_geojson">
									<input type="hidden" name="import_id" value="<?php echo esc_attr( $import_id ); ?>">
									<?php wp_nonce_field( 'CGOP_gis_delete_direct_geojson_' . $import_id ); ?>
									<button type="submit" class="button button-link-delete" onclick="return confirm('<?php echo esc_attr__( 'Delete this uploaded GeoJSON map? Position map assignments that point to it will be cleared.', 'county-gop-core' ); ?>');">
										<?php esc_html_e( 'Delete map', 'county-gop-core' ); ?>
									</button>
								</form>
							</td>
						</tr>
					<?php endforeach; ?>
				</tbody>
			</table>
		<?php else : ?>
			<p><?php esc_html_e( 'No direct GeoJSON uploads have been imported yet.', 'county-gop-core' ); ?></p>
		<?php endif; ?>

		<h3 style="margin-top:1.5rem"><?php esc_html_e( 'Import GeoJSON File', 'county-gop-core' ); ?></h3>
		<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" enctype="multipart/form-data">
			<input type="hidden" name="action" value="CGOP_gis_import_direct_geojson">
			<?php wp_nonce_field( 'CGOP_gis_import_direct_geojson' ); ?>
			<table class="form-table" role="presentation">
				<tr>
					<th scope="row"><label for="direct_geojson_label"><?php esc_html_e( 'Map label', 'county-gop-core' ); ?></label></th>
					<td>
						<input class="regular-text" id="direct_geojson_label" name="label" type="text" required>
						<p class="description"><?php esc_html_e( 'Shown in Position Keys map dropdowns under Uploaded GeoJSON.', 'county-gop-core' ); ?></p>
					</td>
				</tr>
				<tr>
					<th scope="row"><label for="direct_geojson_slug"><?php esc_html_e( 'File slug', 'county-gop-core' ); ?></label></th>
					<td>
						<input class="regular-text" id="direct_geojson_slug" name="slug" type="text">
						<p class="description"><?php esc_html_e( 'Optional. Leave blank to generate from the label. Output path: imported/{slug}.geojson.', 'county-gop-core' ); ?></p>
					</td>
				</tr>
				<tr>
					<th scope="row"><label for="direct_geojson_file"><?php esc_html_e( 'GeoJSON file', 'county-gop-core' ); ?></label></th>
					<td>
						<input id="direct_geojson_file" name="geojson_file" type="file" accept=".geojson,.json,application/geo+json,application/json" required>
						<p class="description"><?php esc_html_e( 'Accepted JSON types: FeatureCollection, Feature, Polygon, or MultiPolygon.', 'county-gop-core' ); ?></p>
					</td>
				</tr>
			</table>
			<?php submit_button( __( 'Import GeoJSON Map', 'county-gop-core' ), 'secondary' ); ?>
		</form>
	</section>
	<?php
}

/**
 * Validate decoded GeoJSON enough for local map rendering.
 *
 * @param mixed $geojson Decoded JSON.
 * @return true|WP_Error
 */
function CGOP_gis_validate_direct_geojson( $geojson ) {
	if ( ! is_array( $geojson ) || empty( $geojson['type'] ) ) {
		return new WP_Error( 'invalid_geojson', __( 'Uploaded file is not valid GeoJSON.', 'county-gop-core' ) );
	}

	$type = (string) $geojson['type'];
	if ( 'FeatureCollection' === $type ) {
		if ( ! isset( $geojson['features'] ) || ! is_array( $geojson['features'] ) ) {
			return new WP_Error( 'invalid_feature_collection', __( 'FeatureCollection must include a features array.', 'county-gop-core' ) );
		}
		return true;
	}

	if ( 'Feature' === $type ) {
		if ( empty( $geojson['geometry'] ) || ! is_array( $geojson['geometry'] ) ) {
			return new WP_Error( 'invalid_feature', __( 'Feature must include geometry.', 'county-gop-core' ) );
		}
		return true;
	}

	if ( in_array( $type, array( 'Polygon', 'MultiPolygon' ), true ) ) {
		return true;
	}

	return new WP_Error( 'unsupported_geojson_type', __( 'Only FeatureCollection, Feature, Polygon, and MultiPolygon GeoJSON uploads are supported.', 'county-gop-core' ) );
}

/**
 * Handle importing a direct GeoJSON file.
 */
function CGOP_gis_handle_import_direct_geojson() {
	if ( ! current_user_can( CGOP_GIS_IMPORT_CAP ) ) {
		wp_die( esc_html__( 'You do not have permission to manage GIS boundary imports.', 'county-gop-core' ) );
	}

	check_admin_referer( 'CGOP_gis_import_direct_geojson' );

	$label = sanitize_text_field( wp_unslash( $_POST['label'] ?? '' ) );
	$slug  = sanitize_title( wp_unslash( $_POST['slug'] ?? '' ) );
	if ( '' === $label ) {
		CGOP_gis_redirect_with_error( __( 'Map label is required.', 'county-gop-core' ) );
	}
	if ( '' === $slug ) {
		$slug = sanitize_title( $label );
	}
	if ( '' === $slug ) {
		$slug = 'uploaded-map';
	}

	if ( empty( $_FILES['geojson_file'] ) || ! is_array( $_FILES['geojson_file'] ) ) {
		CGOP_gis_redirect_with_error( __( 'GeoJSON file is required.', 'county-gop-core' ) );
	}

	$file = $_FILES['geojson_file'];
	if ( ! empty( $file['error'] ) ) {
		CGOP_gis_redirect_with_error( __( 'The GeoJSON upload failed.', 'county-gop-core' ) );
	}

	$name = sanitize_file_name( (string) ( $file['name'] ?? '' ) );
	if ( ! preg_match( '/\.(geojson|json)$/i', $name ) ) {
		CGOP_gis_redirect_with_error( __( 'Please upload a .geojson or .json file.', 'county-gop-core' ) );
	}

	$tmp = (string) ( $file['tmp_name'] ?? '' );
	if ( ! $tmp || ! is_uploaded_file( $tmp ) ) {
		CGOP_gis_redirect_with_error( __( 'Could not read the uploaded GeoJSON file.', 'county-gop-core' ) );
	}

	$raw = file_get_contents( $tmp );
	if ( false === $raw ) {
		CGOP_gis_redirect_with_error( __( 'Could not read the uploaded GeoJSON file.', 'county-gop-core' ) );
	}

	$geojson    = json_decode( $raw, true );
	$validation = CGOP_gis_validate_direct_geojson( $geojson );
	if ( is_wp_error( $validation ) ) {
		CGOP_gis_redirect_with_error( $validation->get_error_message() );
	}

	$imports = CGOP_gis_get_direct_geojson_imports();
	$id      = CGOP_gis_generate_direct_import_id();
	$slug    = CGOP_gis_get_unique_direct_import_slug( $slug, $id, $imports );
	$dir     = trailingslashit( CGOP_gis_get_generated_dir() ) . 'imported';

	if ( ! wp_mkdir_p( $dir ) ) {
		CGOP_gis_redirect_with_error( __( 'Could not create the uploaded GeoJSON output directory.', 'county-gop-core' ) );
	}

	$relative = 'imported/' . $slug . '.geojson';
	$path     = trailingslashit( $dir ) . $slug . '.geojson';
	$json     = wp_json_encode( $geojson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
	if ( ! $json || false === file_put_contents( $path, $json ) ) {
		CGOP_gis_redirect_with_error( __( 'Could not write the uploaded GeoJSON file.', 'county-gop-core' ) );
	}

	$imports[ $id ] = array(
		'id'          => $id,
		'label'       => $label,
		'output_file' => $relative,
		'file_hash'   => md5( $json ),
		'created_at'  => current_time( 'mysql' ),
		'updated_at'  => current_time( 'mysql' ),
	);
	update_option( CGOP_GIS_DIRECT_IMPORTS_OPTION, $imports, false );

	$status          = get_option( CGOP_GIS_IMPORT_STATUS_OPTION, array() );
	$manifest_result = CGOP_gis_write_manifest( is_array( $status ) ? $status : array() );
	if ( is_wp_error( $manifest_result ) ) {
		CGOP_gis_redirect_with_error( $manifest_result->get_error_message() );
	}

	wp_safe_redirect( add_query_arg( 'CGOP_gis_direct_imported', '1', CGOP_gis_get_admin_url() ) );
	exit;
}
add_action( 'admin_post_CGOP_gis_import_direct_geojson', 'CGOP_gis_handle_import_direct_geojson' );

/**
 * Generate a unique direct upload slug.
 *
 * @param string  $slug Existing slug.
 * @param string  $import_id Import ID.
 * @param array[] $imports Existing imports.
 * @return string
 */
function CGOP_gis_get_unique_direct_import_slug( $slug, $import_id, $imports ) {
	$base      = sanitize_title( $slug ) ?: 'uploaded-map';
	$candidate = $base;
	$i         = 2;

	while ( CGOP_gis_direct_import_output_exists( 'imported/' . $candidate . '.geojson', $imports, $import_id ) ) {
		$candidate = $base . '-' . $i;
		$i++;
	}

	return $candidate;
}

/**
 * Determine whether a direct import output file is already registered.
 *
 * @param string  $output_file Output file path.
 * @param array[] $imports Existing imports.
 * @param string  $exclude_id Import ID to ignore.
 * @return bool
 */
function CGOP_gis_direct_import_output_exists( $output_file, $imports, $exclude_id = '' ) {
	foreach ( $imports as $id => $import ) {
		if ( '' !== $exclude_id && (string) $id === (string) $exclude_id ) {
			continue;
		}
		if ( ltrim( (string) ( $import['output_file'] ?? '' ), '/' ) === $output_file ) {
			return true;
		}
	}

	return false;
}

/**
 * Clear Position Key assignments and map boundary rows for a deleted direct import.
 *
 * @param string $import_id Direct import ID.
 * @param string $output_file Relative output file.
 * @return int Number of map boundary rows deleted.
 */
function CGOP_gis_clear_deleted_direct_import_assignments( $import_id, $output_file ) {
	$refs = array(
		'direct:' . (string) $import_id,
		CGOP_gis_get_generated_file_url( $output_file ),
	);
	$refs = array_values( array_unique( array_filter( array_map( 'strval', $refs ) ) ) );

	global $wpdb;
	$map_table      = CGOP_map_boundaries_table();
	$position_table = CGOP_position_keys_table();
	$deleted        = 0;

	// County scope for position_keys updates.
	$pk_scope = function_exists( 'cgop_get_table_county_scope' ) ? cgop_get_table_county_scope( $position_table ) : false;
	if ( null === $pk_scope ) {
		return 0; // Column exists but no county context — skip to avoid cross-county writes.
	}

	foreach ( $refs as $ref ) {
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$map_ids = $wpdb->get_col(
			$wpdb->prepare( "SELECT map_id FROM `{$map_table}` WHERE json_file = %s", $ref )
		);

		foreach ( $map_ids as $map_id ) {
			if ( false !== $pk_scope ) {
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
				$wpdb->update( $position_table, array( 'live_in_map' => '' ), array( 'live_in_map' => $map_id, 'county_id' => $pk_scope ), array( '%s' ), array( '%s', '%s' ) );
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
				$wpdb->update( $position_table, array( 'voting_area_map' => '' ), array( 'voting_area_map' => $map_id, 'county_id' => $pk_scope ), array( '%s' ), array( '%s', '%s' ) );
			} else {
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
				$wpdb->update( $position_table, array( 'live_in_map' => '' ), array( 'live_in_map' => $map_id ), array( '%s' ), array( '%s' ) );
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
				$wpdb->update( $position_table, array( 'voting_area_map' => '' ), array( 'voting_area_map' => $map_id ), array( '%s' ), array( '%s' ) );
			}

			if ( CGOP_delete_map_boundary( $map_id ) ) {
				$deleted++;
			}
		}
	}

	CGOP_position_keys_clear_cache();

	return $deleted;
}

/**
 * Handle deleting a direct uploaded GeoJSON import.
 */
function CGOP_gis_handle_delete_direct_geojson() {
	if ( ! current_user_can( CGOP_GIS_IMPORT_CAP ) ) {
		wp_die( esc_html__( 'You do not have permission to manage GIS boundary imports.', 'county-gop-core' ) );
	}

	$import_id = sanitize_text_field( wp_unslash( $_POST['import_id'] ?? '' ) );
	check_admin_referer( 'CGOP_gis_delete_direct_geojson_' . $import_id );

	$imports = CGOP_gis_get_direct_geojson_imports();
	if ( ! $import_id || ! isset( $imports[ $import_id ] ) ) {
		CGOP_gis_redirect_with_error( __( 'Uploaded GeoJSON import not found.', 'county-gop-core' ) );
	}

	$output_file = ltrim( (string) ( $imports[ $import_id ]['output_file'] ?? '' ), '/' );
	if ( $output_file ) {
		$base = wp_normalize_path( trailingslashit( CGOP_gis_get_generated_dir() ) );
		$path = wp_normalize_path( $base . $output_file );
		if ( ! str_starts_with( $path, $base ) ) {
			CGOP_gis_redirect_with_error( __( 'Uploaded GeoJSON file path is outside the allowed map directory.', 'county-gop-core' ) );
		}
		if ( file_exists( $path ) && ! wp_delete_file( $path ) ) {
			CGOP_gis_redirect_with_error( __( 'Could not delete the uploaded GeoJSON file.', 'county-gop-core' ) );
		}
	}

	CGOP_gis_clear_deleted_direct_import_assignments( $import_id, $output_file );

	unset( $imports[ $import_id ] );
	update_option( CGOP_GIS_DIRECT_IMPORTS_OPTION, $imports, false );

	$status          = get_option( CGOP_GIS_IMPORT_STATUS_OPTION, array() );
	$manifest_result = CGOP_gis_write_manifest( is_array( $status ) ? $status : array() );
	if ( is_wp_error( $manifest_result ) ) {
		CGOP_gis_redirect_with_error( $manifest_result->get_error_message() );
	}

	wp_safe_redirect( add_query_arg( 'CGOP_gis_direct_deleted', '1', CGOP_gis_get_admin_url() ) );
	exit;
}
add_action( 'admin_post_CGOP_gis_delete_direct_geojson', 'CGOP_gis_handle_delete_direct_geojson' );

/**
 * Render the Composite Maps admin section.
 *
 * @param array[] $composites    Current composite map definitions.
 * @param array[] $source_choices Available Beacon-layer files for source selection.
 */
function CGOP_gis_render_composite_maps_section( $composites, $source_choices ) {
	?>
	<section class="cgop-gis-panel">
		<div class="cgop-gis-panel__header">
			<div>
				<h2><?php esc_html_e( 'Composite Maps', 'county-gop-core' ); ?></h2>
				<p><?php esc_html_e( 'Combine generated GeoJSON files into one assignable map. Composites do not call Beacon and do not dissolve boundaries; they package selected source features together for Position Key map assignments.', 'county-gop-core' ); ?></p>
			</div>
		</div>

	<?php if ( $composites ) : ?>
		<table class="widefat striped cgop-gis-table">
			<thead>
				<tr>
					<th><?php esc_html_e( 'Label', 'county-gop-core' ); ?></th>
					<th><?php esc_html_e( 'Composite ID', 'county-gop-core' ); ?></th>
					<th><?php esc_html_e( 'Output File', 'county-gop-core' ); ?></th>
					<th><?php esc_html_e( 'Sources', 'county-gop-core' ); ?></th>
					<th><?php esc_html_e( 'Updated', 'county-gop-core' ); ?></th>
					<th><?php esc_html_e( 'Actions', 'county-gop-core' ); ?></th>
				</tr>
			</thead>
			<tbody>
				<?php foreach ( $composites as $composite ) : ?>
					<?php
					$c_url     = ! empty( $composite['output_file'] ) ? CGOP_gis_get_generated_file_url( $composite['output_file'] ) : '';
					$c_count   = count( $composite['source_files'] ?? array() );
					$c_updated = $composite['updated_at'] ?? $composite['created_at'] ?? '';
					?>
					<tr>
						<td><strong><?php echo esc_html( $composite['label'] ); ?></strong></td>
						<td><code class="cgop-gis-id"><?php echo esc_html( $composite['id'] ); ?></code></td>
						<td><code><?php echo esc_html( $composite['output_file'] ?? '' ); ?></code></td>
						<td><?php echo esc_html( number_format_i18n( $c_count ) ); ?></td>
						<td><?php echo $c_updated ? esc_html( $c_updated ) : esc_html__( 'Unknown', 'county-gop-core' ); ?></td>
						<td>
							<div class="cgop-gis-actions">
							<?php if ( $c_url ) : ?>
								<a class="button" href="<?php echo esc_url( $c_url ); ?>" target="_blank" rel="noopener">
									<?php esc_html_e( 'View JSON', 'county-gop-core' ); ?>
								</a>
							<?php endif; ?>
							<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
								<input type="hidden" name="action" value="CGOP_gis_rebuild_composite">
								<input type="hidden" name="composite_id" value="<?php echo esc_attr( $composite['id'] ); ?>">
								<?php wp_nonce_field( 'CGOP_gis_rebuild_composite_' . $composite['id'] ); ?>
								<button type="submit" class="button">
									<?php esc_html_e( 'Rebuild', 'county-gop-core' ); ?>
								</button>
							</form>
							<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
								<input type="hidden" name="action" value="CGOP_gis_delete_composite">
								<input type="hidden" name="composite_id" value="<?php echo esc_attr( $composite['id'] ); ?>">
								<?php wp_nonce_field( 'CGOP_gis_delete_composite_' . $composite['id'] ); ?>
								<button type="submit" class="button button-link-delete"
									onclick="return confirm(<?php echo esc_attr( wp_json_encode( __( 'Delete this composite map? Source files will not be deleted. Position Key assignments that pointed to this composite may stop rendering until reassigned.', 'county-gop-core' ) ) ); ?>)">
									<?php esc_html_e( 'Delete', 'county-gop-core' ); ?>
								</button>
							</form>
							</div>
						</td>
					</tr>
				<?php endforeach; ?>
			</tbody>
		</table>
	<?php else : ?>
		<p><?php esc_html_e( 'No composite maps defined yet.', 'county-gop-core' ); ?></p>
	<?php endif; ?>

	<h3><?php esc_html_e( 'Create Composite Map', 'county-gop-core' ); ?></h3>
	<?php if ( ! $source_choices ) : ?>
		<p><?php esc_html_e( 'No generated GeoJSON files are available yet. Regenerate a Beacon layer first to create source files for composites.', 'county-gop-core' ); ?></p>
	<?php else : ?>
		<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
			<?php wp_nonce_field( 'CGOP_gis_create_composite' ); ?>
			<input type="hidden" name="action" value="CGOP_gis_create_composite">
			<table class="form-table" role="presentation">
				<tr>
					<th scope="row"><label for="composite_label"><?php esc_html_e( 'Composite label', 'county-gop-core' ); ?> <span class="cgop-gis-required">*</span></label></th>
					<td>
						<input class="regular-text" id="composite_label" name="composite_label" type="text" value="" required>
						<p class="description"><?php esc_html_e( 'A human-readable name. Used in the Position Keys generated-file dropdown as "Composites - {label}".', 'county-gop-core' ); ?></p>
					</td>
				</tr>
				<tr>
					<th scope="row"><label for="composite_slug"><?php esc_html_e( 'Output slug / filename', 'county-gop-core' ); ?></label></th>
					<td>
						<input class="regular-text" id="composite_slug" name="composite_slug" type="text" value="">
						<p class="description"><?php esc_html_e( 'Optional. Leave blank to generate from the label. Output path: composites/{slug}.geojson inside the generated maps directory.', 'county-gop-core' ); ?></p>
					</td>
				</tr>
				<tr>
					<th scope="row"><?php esc_html_e( 'Source generated files', 'county-gop-core' ); ?> <span class="cgop-gis-required">*</span></th>
					<td>
						<?php
						$current_group = null;
						foreach ( $source_choices as $relative => $choice ) :
							$group = $choice['layer_label'];
							if ( $group !== $current_group ) :
								if ( null !== $current_group ) :
									?>
									</div>
									<?php
								endif;
								?>
								<div class="cgop-gis-source-group"><?php echo esc_html( $group ); ?></div>
								<div class="cgop-gis-source-list">
								<?php
								$current_group = $group;
							endif;
							?>
							<label class="cgop-gis-source-item">
								<input type="checkbox" name="source_files[]" value="<?php echo esc_attr( $relative ); ?>">
								<?php echo esc_html( $choice['label'] ); ?>
								<?php if ( ! empty( $choice['position_id'] ) ) : ?>
									<small>(<?php echo esc_html( $choice['position_id'] ); ?>)</small>
								<?php endif; ?>
							</label>
						<?php endforeach; ?>
						<?php if ( null !== $current_group ) : ?>
							</div>
						<?php endif; ?>
						<p class="description"><?php esc_html_e( 'Select two or more files to combine. The composite FeatureCollection will include all features from each selected file.', 'county-gop-core' ); ?></p>
					</td>
				</tr>
			</table>
			<?php submit_button( __( 'Create Composite Map', 'county-gop-core' ) ); ?>
		</form>
	<?php endif; ?>
	</section>
	<?php
}

/**
 * Handle creating a new composite map.
 */
function CGOP_gis_handle_create_composite() {
	if ( ! current_user_can( CGOP_GIS_IMPORT_CAP ) ) {
		wp_die( esc_html__( 'You do not have permission to manage GIS boundary imports.', 'county-gop-core' ) );
	}

	check_admin_referer( 'CGOP_gis_create_composite' );

	$label = sanitize_text_field( wp_unslash( $_POST['composite_label'] ?? '' ) );
	if ( '' === $label ) {
		CGOP_gis_redirect_with_error( __( 'Composite label is required.', 'county-gop-core' ) );
	}

	$slug = isset( $_POST['composite_slug'] ) ? CGOP_gis_slug( sanitize_text_field( wp_unslash( $_POST['composite_slug'] ) ) ) : '';
	if ( '' === $slug ) {
		$slug = CGOP_gis_slug( $label );
	}
	if ( '' === $slug ) {
		$slug = 'composite-' . time();
	}

	$raw_sources = isset( $_POST['source_files'] ) && is_array( $_POST['source_files'] ) ? $_POST['source_files'] : array();
	if ( empty( $raw_sources ) ) {
		CGOP_gis_redirect_with_error( __( 'At least one source file must be selected.', 'county-gop-core' ) );
	}

	$source_files = array();
	foreach ( $raw_sources as $raw_file ) {
		$relative   = ltrim( sanitize_text_field( wp_unslash( $raw_file ) ), '/' );
		$validation = CGOP_gis_validate_composite_source_file( $relative );
		if ( is_wp_error( $validation ) ) {
			CGOP_gis_redirect_with_error( $validation->get_error_message() );
		}
		$source_files[] = $relative;
	}

	$composites   = CGOP_gis_get_composite_maps();
	$composite_id = CGOP_gis_generate_composite_id();
	$slug         = CGOP_gis_get_unique_composite_slug( $slug, $composite_id, $composites );
	$result       = CGOP_gis_save_composite( $composite_id, $label, $slug, $source_files, $composites );
	if ( is_wp_error( $result ) ) {
		CGOP_gis_redirect_with_error( $result->get_error_message() );
	}

	wp_safe_redirect( add_query_arg( 'CGOP_gis_composite_created', '1', CGOP_gis_get_admin_url() ) );
	exit;
}
add_action( 'admin_post_CGOP_gis_create_composite', 'CGOP_gis_handle_create_composite' );

/**
 * Handle rebuilding an existing composite map from its saved source files.
 */
function CGOP_gis_handle_rebuild_composite() {
	if ( ! current_user_can( CGOP_GIS_IMPORT_CAP ) ) {
		wp_die( esc_html__( 'You do not have permission to manage GIS boundary imports.', 'county-gop-core' ) );
	}

	$composite_id = isset( $_POST['composite_id'] ) ? sanitize_text_field( wp_unslash( $_POST['composite_id'] ) ) : '';
	check_admin_referer( 'CGOP_gis_rebuild_composite_' . $composite_id );

	if ( '' === $composite_id ) {
		CGOP_gis_redirect_with_error( __( 'Invalid composite map ID.', 'county-gop-core' ) );
	}

	$composites = CGOP_gis_get_composite_maps();
	if ( ! isset( $composites[ $composite_id ] ) ) {
		CGOP_gis_redirect_with_error( __( 'Composite map not found.', 'county-gop-core' ) );
	}

	$composite    = $composites[ $composite_id ];
	$label        = $composite['label'];
	$output_file  = $composite['output_file'] ?? '';
	$slug         = preg_replace( '/\.(geojson|json)$/i', '', basename( $output_file ) );
	$source_files = $composite['source_files'] ?? array();

	if ( empty( $source_files ) ) {
		CGOP_gis_redirect_with_error( __( 'Composite map has no saved source files to rebuild from.', 'county-gop-core' ) );
	}

	$result = CGOP_gis_save_composite( $composite_id, $label, $slug, $source_files, $composites );
	if ( is_wp_error( $result ) ) {
		CGOP_gis_redirect_with_error( $result->get_error_message() );
	}

	wp_safe_redirect( add_query_arg( 'CGOP_gis_composite_rebuilt', '1', CGOP_gis_get_admin_url() ) );
	exit;
}
add_action( 'admin_post_CGOP_gis_rebuild_composite', 'CGOP_gis_handle_rebuild_composite' );

/**
 * Handle deleting a composite map.
 *
 * Deletes the physical file and removes the definition from the option.
 * Does not delete source files. Position Key assignments that pointed to the
 * deleted composite may stop rendering until reassigned.
 */
function CGOP_gis_handle_delete_composite() {
	if ( ! current_user_can( CGOP_GIS_IMPORT_CAP ) ) {
		wp_die( esc_html__( 'You do not have permission to manage GIS boundary imports.', 'county-gop-core' ) );
	}

	$composite_id = isset( $_POST['composite_id'] ) ? sanitize_text_field( wp_unslash( $_POST['composite_id'] ) ) : '';
	check_admin_referer( 'CGOP_gis_delete_composite_' . $composite_id );

	if ( '' === $composite_id ) {
		CGOP_gis_redirect_with_error( __( 'Invalid composite map ID.', 'county-gop-core' ) );
	}

	$composites = CGOP_gis_get_composite_maps();
	if ( ! isset( $composites[ $composite_id ] ) ) {
		CGOP_gis_redirect_with_error( __( 'Composite map not found.', 'county-gop-core' ) );
	}

	$output_file    = $composites[ $composite_id ]['output_file'] ?? '';
	$output_shared  = $output_file && CGOP_gis_composite_output_is_shared( $output_file, $composites, $composite_id );
	$clear_url_refs = ! $output_shared;

	CGOP_gis_clear_deleted_composite_assignments( $composite_id, $output_file, $clear_url_refs );

	if ( $output_file && ! $output_shared ) {
		$base      = wp_normalize_path( trailingslashit( CGOP_gis_get_generated_dir() ) );
		$file_path = wp_normalize_path( $base . ltrim( $output_file, '/' ) );
		if ( str_starts_with( $file_path, $base ) && file_exists( $file_path ) ) {
			wp_delete_file( $file_path );
		}
	}

	unset( $composites[ $composite_id ] );
	update_option( CGOP_GIS_COMPOSITE_MAPS_OPTION, $composites, false );

	$status          = get_option( CGOP_GIS_IMPORT_STATUS_OPTION, array() );
	$manifest_result = CGOP_gis_write_manifest( is_array( $status ) ? $status : array() );
	if ( is_wp_error( $manifest_result ) ) {
		CGOP_gis_redirect_with_error( $manifest_result->get_error_message() );
	}

	wp_safe_redirect( add_query_arg( 'CGOP_gis_composite_deleted', '1', CGOP_gis_get_admin_url() ) );
	exit;
}
add_action( 'admin_post_CGOP_gis_delete_composite', 'CGOP_gis_handle_delete_composite' );