<?php
/**
 * AP Family Archive - Sources & repositories management (admin).
 *
 * The curation surface spec-02 Section 2 assigns to Archive: sources and
 * repositories CRUD through the core API (Sources_Table /
 * Repositories_Table, core 0.7.0). Citations attach to people/families
 * via GEDCOM import today; per-record citation editing is the next slice.
 *
 * @package    APFAAR_Family_Archive
 * @subpackage APFAAR_Family_Archive/admin
 * @since      0.1.0
 */

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

/**
 * Sources tab: sources + repositories list/add/edit/delete.
 *
 * @since 0.1.0
 */
class APFAAR_Admin_Sources {

	/**
	 * Whether the booted core carries the sources extension.
	 *
	 * @return bool
	 */
	public static function available() {
		return class_exists( 'AncestorPress\Core\Sources_Table' ) && class_exists( 'AncestorPress\Core\Repositories_Table' ) && class_exists( 'AncestorPress\Core\Citations_Table' );
	}

	/**
	 * Render the Sources tab.
	 *
	 * @param  string $gedcom Working tree.
	 * @return void
	 */
	public static function render( $gedcom ) {
		if ( ! self::available() ) {
			echo '<div class="apfa-notice-amber">' . esc_html__( 'The data core on this site predates the sources extension (core 0.7.0). Update the suite plugins and reload.', 'ap-family-archive' ) . '</div>';
			return;
		}

		// phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only view routing.
		$action = isset( $_GET['apfaar_action'] ) ? sanitize_key( wp_unslash( $_GET['apfaar_action'] ) ) : '';
		$sid    = isset( $_GET['apfaar_sid'] ) ? sanitize_text_field( wp_unslash( $_GET['apfaar_sid'] ) ) : '';
		$rid    = isset( $_GET['apfaar_rid'] ) ? sanitize_text_field( wp_unslash( $_GET['apfaar_rid'] ) ) : '';
		// phpcs:enable

		if ( 'source_add' === $action || ( 'source_edit' === $action && '' !== $sid ) ) {
			self::render_source_form( $gedcom, 'source_edit' === $action ? $sid : '' );
			return;
		}
		if ( 'repo_add' === $action || ( 'repo_edit' === $action && '' !== $rid ) ) {
			self::render_repo_form( $gedcom, 'repo_edit' === $action ? $rid : '' );
			return;
		}
		self::render_lists( $gedcom );
	}

	/**
	 * URL of the Sources tab with args.
	 *
	 * @param  string $gedcom Working tree.
	 * @param  array  $args   Extra query args.
	 * @return string
	 */
	public static function url( $gedcom, $args = array() ) {
		return add_query_arg(
			array_merge(
				array(
					'page' => 'apfaar',
					'tab'  => 'sources',
					'tree' => rawurlencode( $gedcom ),
				),
				$args
			),
			admin_url( 'admin.php' )
		);
	}

	/**
	 * Sources + repositories lists.
	 *
	 * @param  string $gedcom Working tree.
	 * @return void
	 */
	private static function render_lists( $gedcom ) {
		$sources = \AncestorPress\Core\Sources_Table::get_by_tree( $gedcom );

		echo '<div class="apfa-section-card apfa-table-card" style="padding:16px">';
		echo '<p><a class="button button-primary" href="' . esc_url( self::url( $gedcom, array( 'apfaar_action' => 'source_add' ) ) ) . '">' . esc_html__( 'Add source', 'ap-family-archive' ) . '</a> ';
		/* translators: %s: number of sources. */
		echo '<span class="apfa-filter-count">' . esc_html( sprintf( __( '%s sources in this tree', 'ap-family-archive' ), number_format_i18n( is_array( $sources ) ? count( $sources ) : 0 ) ) ) . '</span></p>';

		if ( ! empty( $sources ) ) {
			echo '<table class="widefat striped"><thead><tr>'
				. '<th>' . esc_html__( 'ID', 'ap-family-archive' ) . '</th>'
				. '<th>' . esc_html__( 'Title', 'ap-family-archive' ) . '</th>'
				. '<th>' . esc_html__( 'Author', 'ap-family-archive' ) . '</th>'
				. '<th>' . esc_html__( 'Repository', 'ap-family-archive' ) . '</th>'
				. '<th>' . esc_html__( 'Citations', 'ap-family-archive' ) . '</th>'
				. '<th>' . esc_html__( 'Actions', 'ap-family-archive' ) . '</th>'
				. '</tr></thead><tbody>';
			foreach ( $sources as $s ) {
				$cites      = count( (array) \AncestorPress\Core\Citations_Table::get_by_source( $gedcom, $s->sourceID ) );
				$delete_url = wp_nonce_url(
					admin_url( 'admin-post.php?action=apfaar_source_delete&tree=' . rawurlencode( $gedcom ) . '&apfaar_sid=' . rawurlencode( $s->sourceID ) ),
					'apfaar_source_delete_' . $s->sourceID
				);
				echo '<tr>'
					. '<td><span class="apfa-gedcom-ref">' . esc_html( $s->sourceID ) . '</span></td>'
					. '<td>' . esc_html( (string) $s->title ) . '</td>'
					. '<td>' . esc_html( (string) $s->author ) . '</td>'
					. '<td>' . esc_html( (string) $s->repoID ) . '</td>'
					. '<td>' . esc_html( number_format_i18n( $cites ) ) . '</td>'
					. '<td><a class="apfa-action-edit" href="' . esc_url( self::url( $gedcom, array( 'apfaar_action' => 'source_edit', 'apfaar_sid' => $s->sourceID ) ) ) . '">' . esc_html__( 'Edit', 'ap-family-archive' ) . '</a> | '
						. '<a class="apfa-action-delete" href="' . esc_url( $delete_url ) . '" onclick="return confirm(\'' . esc_js( __( 'Delete this source AND all its citations? This cannot be undone.', 'ap-family-archive' ) ) . '\')">' . esc_html__( 'Delete', 'ap-family-archive' ) . '</a></td>'
					. '</tr>';
			}
			echo '</tbody></table>';
		} else {
			echo '<p>' . esc_html__( 'No sources yet. Add one, or import a GEDCOM that carries source records.', 'ap-family-archive' ) . '</p>';
		}
		echo '</div>';

		// Repositories.
		$repos = \AncestorPress\Core\Repositories_Table::get_by_tree( $gedcom );
		echo '<div class="apfa-section-card apfa-table-card" style="padding:16px">';
		echo '<h2 class="apfa-options-section-title">' . esc_html__( 'Repositories', 'ap-family-archive' ) . '</h2>';
		echo '<p class="description">' . esc_html__( 'The archives, libraries and offices that hold your sources. A source links to a repository by its R-number.', 'ap-family-archive' ) . '</p>';
		echo '<p><a class="button button-secondary" href="' . esc_url( self::url( $gedcom, array( 'apfaar_action' => 'repo_add' ) ) ) . '">' . esc_html__( 'Add repository', 'ap-family-archive' ) . '</a></p>';
		if ( ! empty( $repos ) ) {
			echo '<table class="widefat striped"><thead><tr>'
				. '<th>' . esc_html__( 'ID', 'ap-family-archive' ) . '</th>'
				. '<th>' . esc_html__( 'Name', 'ap-family-archive' ) . '</th>'
				. '<th>' . esc_html__( 'Location', 'ap-family-archive' ) . '</th>'
				. '<th>' . esc_html__( 'Actions', 'ap-family-archive' ) . '</th>'
				. '</tr></thead><tbody>';
			foreach ( $repos as $r ) {
				$loc        = implode( ', ', array_filter( array( (string) $r->city, (string) $r->country ) ) );
				$delete_url = wp_nonce_url(
					admin_url( 'admin-post.php?action=apfaar_repo_delete&tree=' . rawurlencode( $gedcom ) . '&apfaar_rid=' . rawurlencode( $r->repoID ) ),
					'apfaar_repo_delete_' . $r->repoID
				);
				echo '<tr>'
					. '<td><span class="apfa-gedcom-ref">' . esc_html( $r->repoID ) . '</span></td>'
					. '<td>' . esc_html( (string) $r->reponame ) . '</td>'
					. '<td>' . esc_html( $loc ) . '</td>'
					. '<td><a class="apfa-action-edit" href="' . esc_url( self::url( $gedcom, array( 'apfaar_action' => 'repo_edit', 'apfaar_rid' => $r->repoID ) ) ) . '">' . esc_html__( 'Edit', 'ap-family-archive' ) . '</a> | '
						. '<a class="apfa-action-delete" href="' . esc_url( $delete_url ) . '" onclick="return confirm(\'' . esc_js( __( 'Delete this repository? Sources keep working; their repository link goes stale.', 'ap-family-archive' ) ) . '\')">' . esc_html__( 'Delete', 'ap-family-archive' ) . '</a></td>'
					. '</tr>';
			}
			echo '</tbody></table>';
		}
		echo '</div>';
	}

	/**
	 * Add/edit source form.
	 *
	 * @param  string $gedcom Working tree.
	 * @param  string $sid    Source to edit ('' = add).
	 * @return void
	 */
	private static function render_source_form( $gedcom, $sid ) {
		$source = '' !== $sid ? \AncestorPress\Core\Sources_Table::get_by_ref( $gedcom, $sid ) : null;
		if ( '' !== $sid && ! $source ) {
			echo '<div class="apfa-notice-amber">' . esc_html__( 'Source not found in this tree.', 'ap-family-archive' ) . '</div>';
			return;
		}
		$val = function ( $field ) use ( $source ) {
			return $source ? (string) ( $source->{$field} ?? '' ) : '';
		};

		echo '<div class="apfa-section-card">';
		echo '<h2 class="apfa-options-section-title">'
			. ( $source
				? esc_html__( 'Edit source', 'ap-family-archive' ) . ' <span class="apfa-gedcom-ref">' . esc_html( $sid ) . '</span>'
				: esc_html__( 'Add source', 'ap-family-archive' ) )
			. '</h2>';

		echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '">';
		wp_nonce_field( 'apfaar_source_save' );
		echo '<input type="hidden" name="action" value="apfaar_source_save" />';
		echo '<input type="hidden" name="tree" value="' . esc_attr( $gedcom ) . '" />';
		echo '<input type="hidden" name="apfaar_sid" value="' . esc_attr( $sid ) . '" />';

		self::text_row( __( 'Title', 'ap-family-archive' ), 'title', $val( 'title' ) );
		self::text_row( __( 'Author', 'ap-family-archive' ), 'author', $val( 'author' ) );
		self::text_row( __( 'Publisher', 'ap-family-archive' ), 'publisher', $val( 'publisher' ) );
		self::text_row( __( 'Publication date', 'ap-family-archive' ), 'pubdate', $val( 'pubdate' ) );
		self::text_row( __( 'Repository ID', 'ap-family-archive' ), 'repoID', $val( 'repoID' ), 'R1' );
		self::text_row( __( 'Call number', 'ap-family-archive' ), 'callnum', $val( 'callnum' ) );
		echo '<div class="apfa-field-row"><span class="apfa-field-row-label">' . esc_html__( 'Transcript / extract', 'ap-family-archive' ) . '</span>'
			. '<span class="apfa-field-row-control"><textarea name="actualtext" rows="4" style="width:100%;max-width:480px">' . esc_textarea( $val( 'actualtext' ) ) . '</textarea></span></div>';

		echo '<div class="apfa-save-bar">';
		submit_button( $source ? __( 'Save changes', 'ap-family-archive' ) : __( 'Add source', 'ap-family-archive' ), 'primary', 'submit', false );
		echo ' <a class="button button-secondary" href="' . esc_url( self::url( $gedcom ) ) . '">' . esc_html__( 'Back to list', 'ap-family-archive' ) . '</a>';
		echo '</div></form></div>';
	}

	/**
	 * Add/edit repository form.
	 *
	 * @param  string $gedcom Working tree.
	 * @param  string $rid    Repository to edit ('' = add).
	 * @return void
	 */
	private static function render_repo_form( $gedcom, $rid ) {
		$repo = '' !== $rid ? \AncestorPress\Core\Repositories_Table::get_by_ref( $gedcom, $rid ) : null;
		if ( '' !== $rid && ! $repo ) {
			echo '<div class="apfa-notice-amber">' . esc_html__( 'Repository not found in this tree.', 'ap-family-archive' ) . '</div>';
			return;
		}
		$val = function ( $field ) use ( $repo ) {
			return $repo ? (string) ( $repo->{$field} ?? '' ) : '';
		};

		echo '<div class="apfa-section-card">';
		echo '<h2 class="apfa-options-section-title">'
			. ( $repo
				? esc_html__( 'Edit repository', 'ap-family-archive' ) . ' <span class="apfa-gedcom-ref">' . esc_html( $rid ) . '</span>'
				: esc_html__( 'Add repository', 'ap-family-archive' ) )
			. '</h2>';

		echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '">';
		wp_nonce_field( 'apfaar_repo_save' );
		echo '<input type="hidden" name="action" value="apfaar_repo_save" />';
		echo '<input type="hidden" name="tree" value="' . esc_attr( $gedcom ) . '" />';
		echo '<input type="hidden" name="apfaar_rid" value="' . esc_attr( $rid ) . '" />';

		self::text_row( __( 'Name', 'ap-family-archive' ), 'reponame', $val( 'reponame' ) );
		self::text_row( __( 'Address', 'ap-family-archive' ), 'address', $val( 'address' ) );
		self::text_row( __( 'City', 'ap-family-archive' ), 'city', $val( 'city' ) );
		self::text_row( __( 'County / state', 'ap-family-archive' ), 'state', $val( 'state' ) );
		self::text_row( __( 'Postcode', 'ap-family-archive' ), 'zip', $val( 'zip' ) );
		self::text_row( __( 'Country', 'ap-family-archive' ), 'country', $val( 'country' ) );
		self::text_row( __( 'Phone', 'ap-family-archive' ), 'phone', $val( 'phone' ) );
		self::text_row( __( 'Email', 'ap-family-archive' ), 'email', $val( 'email' ) );
		self::text_row( __( 'Website', 'ap-family-archive' ), 'url', $val( 'url' ) );

		echo '<div class="apfa-save-bar">';
		submit_button( $repo ? __( 'Save changes', 'ap-family-archive' ) : __( 'Add repository', 'ap-family-archive' ), 'primary', 'submit', false );
		echo ' <a class="button button-secondary" href="' . esc_url( self::url( $gedcom ) ) . '">' . esc_html__( 'Back to list', 'ap-family-archive' ) . '</a>';
		echo '</div></form></div>';
	}

	/**
	 * One labelled text input row.
	 *
	 * @param  string $label Row label.
	 * @param  string $name  Field name.
	 * @param  string $value Current value.
	 * @param  string $hint  Optional placeholder.
	 * @return void
	 */
	private static function text_row( $label, $name, $value, $hint = '' ) {
		echo '<div class="apfa-field-row"><span class="apfa-field-row-label">' . esc_html( $label ) . '</span>'
			. '<span class="apfa-field-row-control"><input type="text" name="' . esc_attr( $name ) . '" value="' . esc_attr( $value ) . '" placeholder="' . esc_attr( $hint ) . '" /></span></div>';
	}

	// ── Citations panel (embedded in person/family edit screens) ──────────────

	/**
	 * The "Sources for this record" panel: existing citations with remove
	 * links + an add form. Rendered inside the person/family edit screens -
	 * the "back this fact with a source" workflow.
	 *
	 * @param  string $gedcom    Working tree.
	 * @param  string $persfamID Person or family reference.
	 * @param  string $back_tab  Tab to return to (people|families).
	 * @return void
	 */
	public static function citations_panel( $gedcom, $persfamID, $back_tab ) {
		if ( ! self::available() ) {
			return;
		}
		echo '<h3 class="apfa-section-heading">' . esc_html__( 'Sources for this record', 'ap-family-archive' ) . '</h3>';

		// Events of this record that citations can attach to (only events
		// carrying an eventID are addressable; imported events without one
		// need the core-side eventID mint, a recorded core backlog item).
		$events       = APFAAR_People::events_for( $gedcom, $persfamID );
		$citable      = array();
		$event_labels = array();
		foreach ( $events as $event ) {
			if ( ! empty( $event->eventID ) ) {
				$text = $event->label . ( ! empty( $event->eventdate ) ? ' (' . $event->eventdate . ')' : '' );
				$citable[ (string) $event->eventID ]      = $text;
				$event_labels[ (string) $event->eventID ] = $text;
			}
		}

		$citations = \AncestorPress\Core\Citations_Table::get_for_persfam( $gedcom, $persfamID );
		if ( ! empty( $citations ) ) {
			echo '<table class="widefat striped"><tbody>';
			foreach ( $citations as $cit ) {
				$label = '' !== (string) $cit->source_title ? $cit->source_title : $cit->sourceID;
				$bits  = array_filter( array(
					'' !== (string) $cit->page ? $cit->page : '',
					( null !== $cit->quay && '' !== (string) $cit->quay )
						/* translators: %d: GEDCOM quality rating 0-3. */
						? sprintf( __( 'quality %d', 'ap-family-archive' ), (int) $cit->quay ) : '',
					( ! empty( $cit->eventID ) && isset( $event_labels[ (string) $cit->eventID ] ) )
						/* translators: %s: event label the citation backs. */
						? sprintf( __( 'for: %s', 'ap-family-archive' ), $event_labels[ (string) $cit->eventID ] ) : '',
				) );
				$remove_url = wp_nonce_url(
					admin_url( 'admin-post.php?action=apfaar_cite_delete&tree=' . rawurlencode( $gedcom ) . '&cite=' . (int) $cit->id . '&back=' . rawurlencode( $back_tab ) . '&ref=' . rawurlencode( $persfamID ) ),
					'apfaar_cite_delete_' . (int) $cit->id
				);
				echo '<tr><td>' . esc_html( $label ) . ' <span class="apfa-gedcom-ref">' . esc_html( $cit->sourceID ) . '</span>'
					. ( $bits ? ' <span class="description">(' . esc_html( implode( ', ', $bits ) ) . ')</span>' : '' )
					. ( '' !== (string) $cit->citetext ? '<br /><em>' . esc_html( wp_trim_words( $cit->citetext, 18 ) ) . '</em>' : '' )
					. '</td>'
					. '<td style="width:80px"><a class="apfa-action-delete" href="' . esc_url( $remove_url ) . '">' . esc_html__( 'Remove', 'ap-family-archive' ) . '</a></td></tr>';
			}
			echo '</tbody></table>';
		} else {
			echo '<p class="description">' . esc_html__( 'No sources cited yet.', 'ap-family-archive' ) . '</p>';
		}

		$sources = \AncestorPress\Core\Sources_Table::get_by_tree( $gedcom );
		if ( empty( $sources ) ) {
			echo '<p class="description">' . esc_html__( 'Add sources on the Sources tab first, then cite them here.', 'ap-family-archive' ) . '</p>';
			return;
		}

		echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" style="margin-top:10px">';
		wp_nonce_field( 'apfaar_cite_add' );
		echo '<input type="hidden" name="action" value="apfaar_cite_add" />';
		echo '<input type="hidden" name="tree" value="' . esc_attr( $gedcom ) . '" />';
		echo '<input type="hidden" name="ref" value="' . esc_attr( $persfamID ) . '" />';
		echo '<input type="hidden" name="back" value="' . esc_attr( $back_tab ) . '" />';
		echo '<label>' . esc_html__( 'Source', 'ap-family-archive' ) . ' <select name="cite_source" required>';
		echo '<option value="">' . esc_html__( '- choose -', 'ap-family-archive' ) . '</option>';
		foreach ( $sources as $s ) {
			$title = '' !== (string) $s->title ? wp_trim_words( $s->title, 8 ) : $s->sourceID;
			echo '<option value="' . esc_attr( $s->sourceID ) . '">' . esc_html( $title . ' (' . $s->sourceID . ')' ) . '</option>';
		}
		echo '</select></label> ';
		if ( ! empty( $citable ) ) {
			echo '<label>' . esc_html__( 'Applies to', 'ap-family-archive' ) . ' <select name="cite_event">';
			echo '<option value="">' . esc_html__( 'this whole record', 'ap-family-archive' ) . '</option>';
			foreach ( $citable as $event_id => $text ) {
				echo '<option value="' . esc_attr( $event_id ) . '">' . esc_html( $text ) . '</option>';
			}
			echo '</select></label> ';
		}
		echo '<label>' . esc_html__( 'Where in source', 'ap-family-archive' ) . ' <input type="text" name="cite_page" placeholder="' . esc_attr__( 'e.g. p.44, entry 312', 'ap-family-archive' ) . '" /></label> ';
		echo '<label>' . esc_html__( 'Quality', 'ap-family-archive' ) . ' <select name="cite_quay">';
		echo '<option value="">' . esc_html__( 'unrated', 'ap-family-archive' ) . '</option>';
		foreach ( array(
			3 => __( '3 - primary evidence', 'ap-family-archive' ),
			2 => __( '2 - secondary evidence', 'ap-family-archive' ),
			1 => __( '1 - questionable', 'ap-family-archive' ),
			0 => __( '0 - unreliable/estimated', 'ap-family-archive' ),
		) as $q => $label ) {
			echo '<option value="' . (int) $q . '">' . esc_html( $label ) . '</option>';
		}
		echo '</select></label><br />';
		echo '<label style="display:block;margin-top:6px">' . esc_html__( 'Quoted text (optional)', 'ap-family-archive' )
			. '<br /><textarea name="cite_text" rows="2" style="width:100%;max-width:480px"></textarea></label>';
		submit_button( __( 'Cite source', 'ap-family-archive' ), 'secondary', 'submit', false );
		echo '</form>';
	}

	/**
	 * Add a citation to a person/family (handler body).
	 *
	 * @param  string $gedcom    Working tree.
	 * @param  string $persfamID Person or family reference.
	 * @return string|null       Error message or null on success.
	 */
	public static function add_citation( $gedcom, $persfamID ) {
		// phpcs:disable WordPress.Security.NonceVerification.Missing -- caller (APFAAR_Admin) verified cap + nonce.
		$sourceID = isset( $_POST['cite_source'] ) ? sanitize_text_field( wp_unslash( $_POST['cite_source'] ) ) : '';
		$event_id = isset( $_POST['cite_event'] ) ? sanitize_text_field( wp_unslash( $_POST['cite_event'] ) ) : '';
		$page     = isset( $_POST['cite_page'] ) ? sanitize_text_field( wp_unslash( $_POST['cite_page'] ) ) : '';
		$quay_raw = isset( $_POST['cite_quay'] ) ? sanitize_text_field( wp_unslash( $_POST['cite_quay'] ) ) : '';
		$text     = isset( $_POST['cite_text'] ) ? sanitize_textarea_field( wp_unslash( $_POST['cite_text'] ) ) : '';
		// phpcs:enable

		if ( '' === $sourceID || ! \AncestorPress\Core\Sources_Table::get_by_ref( $gedcom, $sourceID ) ) {
			return __( 'Choose a source from the list.', 'ap-family-archive' );
		}
		// The cited record itself must exist in THIS tree (person or family) -
		// a manipulated request must not attach a citation to a phantom ref.
		if ( ! \AncestorPress\Core\People_Table::get_by_ref( $gedcom, $persfamID )
			&& ! \AncestorPress\Core\Families_Table::get_by_ref( $gedcom, $persfamID ) ) {
			return __( 'That record does not exist in this tree - citation not added.', 'ap-family-archive' );
		}
		if ( '' !== $event_id ) {
			// The chosen event must belong to THIS record.
			$valid = false;
			foreach ( APFAAR_People::events_for( $gedcom, $persfamID ) as $event ) {
				if ( ! empty( $event->eventID ) && (string) $event->eventID === $event_id ) {
					$valid = true;
					break;
				}
			}
			if ( ! $valid ) {
				return __( 'That event does not belong to this record - citation not added.', 'ap-family-archive' );
			}
		}
		$existing = \AncestorPress\Core\Citations_Table::get_for_persfam( $gedcom, $persfamID );
		\AncestorPress\Core\Citations_Table::insert( array(
			'gedcom'     => $gedcom,
			'citationID' => 'C-' . ( count( (array) $existing ) + 1 ) . '-' . $persfamID,
			'persfamID'  => $persfamID,
			'eventID'    => '' !== $event_id ? $event_id : null,
			'sourceID'   => $sourceID,
			'page'       => $page,
			'quay'       => ( '' !== $quay_raw && is_numeric( $quay_raw ) ) ? min( 3, max( 0, (int) $quay_raw ) ) : null,
			'citetext'   => $text,
			'ordernum'   => count( (array) $existing ) + 1,
		) );
		return null;
	}

	// ── Handlers (called from APFAAR_Admin admin-post endpoints) ────────────────

	/**
	 * Save (add or update) a source.
	 *
	 * @param  string $gedcom Working tree.
	 * @return string         The sourceID saved.
	 */
	public static function save_source( $gedcom ) {
		// phpcs:disable WordPress.Security.NonceVerification.Missing -- caller (APFAAR_Admin) verified cap + nonce.
		$sid  = isset( $_POST['apfaar_sid'] ) ? sanitize_text_field( wp_unslash( $_POST['apfaar_sid'] ) ) : '';
		$data = array(
			'title'      => isset( $_POST['title'] ) ? sanitize_textarea_field( wp_unslash( $_POST['title'] ) ) : '',
			'author'     => isset( $_POST['author'] ) ? sanitize_text_field( wp_unslash( $_POST['author'] ) ) : '',
			'publisher'  => isset( $_POST['publisher'] ) ? sanitize_text_field( wp_unslash( $_POST['publisher'] ) ) : '',
			'pubdate'    => isset( $_POST['pubdate'] ) ? sanitize_text_field( wp_unslash( $_POST['pubdate'] ) ) : '',
			'repoID'     => isset( $_POST['repoID'] ) ? sanitize_text_field( wp_unslash( $_POST['repoID'] ) ) : '',
			'callnum'    => isset( $_POST['callnum'] ) ? sanitize_text_field( wp_unslash( $_POST['callnum'] ) ) : '',
			'actualtext' => isset( $_POST['actualtext'] ) ? sanitize_textarea_field( wp_unslash( $_POST['actualtext'] ) ) : '',
		);
		// phpcs:enable

		if ( '' !== $sid ) {
			$existing = \AncestorPress\Core\Sources_Table::get_by_ref( $gedcom, $sid );
			if ( $existing ) {
				\AncestorPress\Core\Sources_Table::update( (int) $existing->id, $data );
			}
			return $sid;
		}
		$data['gedcom']   = $gedcom;
		$data['sourceID'] = \AncestorPress\Core\Sources_Table::next_manual_id( $gedcom );
		\AncestorPress\Core\Sources_Table::insert( $data );
		return $data['sourceID'];
	}

	/**
	 * Delete a source and its citations.
	 *
	 * @param  string $gedcom Working tree.
	 * @param  string $sid    Source reference.
	 * @return void
	 */
	public static function delete_source( $gedcom, $sid ) {
		$existing = \AncestorPress\Core\Sources_Table::get_by_ref( $gedcom, $sid );
		if ( $existing ) {
			\AncestorPress\Core\Sources_Table::delete_cascade( (int) $existing->id );
		}
	}

	/**
	 * Save (add or update) a repository.
	 *
	 * @param  string $gedcom Working tree.
	 * @return string         The repoID saved.
	 */
	public static function save_repo( $gedcom ) {
		// phpcs:disable WordPress.Security.NonceVerification.Missing -- caller (APFAAR_Admin) verified cap + nonce.
		$rid  = isset( $_POST['apfaar_rid'] ) ? sanitize_text_field( wp_unslash( $_POST['apfaar_rid'] ) ) : '';
		$data = array();
		foreach ( array( 'reponame', 'address', 'city', 'state', 'zip', 'country', 'phone', 'email' ) as $field ) {
			$data[ $field ] = isset( $_POST[ $field ] ) ? sanitize_text_field( wp_unslash( $_POST[ $field ] ) ) : '';
		}
		$data['url'] = isset( $_POST['url'] ) ? esc_url_raw( wp_unslash( $_POST['url'] ) ) : '';
		// phpcs:enable

		if ( '' !== $rid ) {
			$existing = \AncestorPress\Core\Repositories_Table::get_by_ref( $gedcom, $rid );
			if ( $existing ) {
				\AncestorPress\Core\Repositories_Table::update( (int) $existing->id, $data );
			}
			return $rid;
		}
		$data['gedcom'] = $gedcom;
		$data['repoID'] = \AncestorPress\Core\Repositories_Table::next_manual_id( $gedcom );
		\AncestorPress\Core\Repositories_Table::insert( $data );
		return $data['repoID'];
	}

	/**
	 * Delete a repository (sources keep their now-stale link).
	 *
	 * @param  string $gedcom Working tree.
	 * @param  string $rid    Repository reference.
	 * @return void
	 */
	public static function delete_repo( $gedcom, $rid ) {
		$existing = \AncestorPress\Core\Repositories_Table::get_by_ref( $gedcom, $rid );
		if ( $existing ) {
			\AncestorPress\Core\Repositories_Table::delete( (int) $existing->id );
		}
	}
}
