<?php
/**
 * KooKoa Folders
 *
 * @package KookoaWPF
 * @license GPL-2.0-or-later
 */

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

/**
 * Migration importer for the FileBird Media library (folders only).
 *
 * FileBird stores its Media folders in two custom tables, distinct from
 * any WordPress taxonomy:
 *   - {prefix}fbv                   (id, name, parent, type, ord, …)
 *   - {prefix}fbv_attachment_folder (folder_id, attachment_id)
 *
 * Only rows with type=0 are imported. type=1 represents FileBird
 * "Collections" (flat, manually curated groupings), which have no direct
 * equivalent in KooKoa Folders and are intentionally skipped in v1.
 *
 * Every direct $wpdb call goes through the WP object cache so the
 * WordPress.DB.DirectDatabaseQuery sniff is satisfied without needing
 * any phpcs:ignore comments. Cache TTLs are short (counts) or zero
 * (one-shot import reads, invalidated explicitly afterwards) so stale
 * data is never surfaced to the user.
 *
 * @since 1.0.5
 */
class KookoaWPF_Importer_FileBird_Media extends KookoaWPF_Importer_Abstract {

	/**
	 * Object cache group used by every query in this class.
	 */
	private const CACHE_GROUP = 'kookoawpf_filebird_media';

	/**
	 * {@inheritDoc}
	 */
	public function get_id(): string {
		return 'filebird-media';
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_name(): string {
		return __( 'FileBird — Media', 'kookoa-folders' );
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_author(): string {
		return 'Ninja Team';
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_target_post_type(): string {
		return 'attachment';
	}

	/**
	 * Whether ALL FileBird tables required for an import exist in the
	 * current site. The importer needs both the folder catalog (wp_fbv)
	 * AND the attachment-folder relations (wp_fbv_attachment_folder);
	 * if either one is missing the source is incomplete and the import
	 * must not run, otherwise empty folders would be created and then
	 * mark_imported() would lock the importer with bad data.
	 *
	 * Cached for an hour: existence flips only on plugin activation /
	 * uninstall.
	 *
	 * @return bool
	 */
	private function tables_exist(): bool {
		$cached = wp_cache_get( 'tables_exist', self::CACHE_GROUP );
		if ( false !== $cached ) {
			return '1' === (string) $cached;
		}

		global $wpdb;
		$required = array(
			$wpdb->prefix . 'fbv',
			$wpdb->prefix . 'fbv_attachment_folder',
		);

		$exists = true;
		foreach ( $required as $table ) {
			// FileBird's custom tables have no WP API equivalent —
			// direct SHOW TABLES query is the only way to verify.
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
			$found = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
			if ( $found !== $table ) {
				$exists = false;
				break;
			}
		}

		wp_cache_set( 'tables_exist', $exists ? '1' : '0', self::CACHE_GROUP, HOUR_IN_SECONDS );
		return $exists;
	}

	/**
	 * {@inheritDoc}
	 */
	public function count_folders(): int {
		if ( ! $this->tables_exist() ) {
			return 0;
		}

		$cached = wp_cache_get( 'count_folders', self::CACHE_GROUP );
		if ( false !== $cached ) {
			return (int) $cached;
		}

		global $wpdb;
		$table = $wpdb->prefix . 'fbv';
		// Counting FileBird's custom wp_fbv table — no WP API equivalent.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$count = (int) $wpdb->get_var(
			$wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE type = %d', $table, 0 )
		);

		wp_cache_set( 'count_folders', $count, self::CACHE_GROUP, MINUTE_IN_SECONDS );
		return $count;
	}

	/**
	 * {@inheritDoc}
	 */
	public function count_items(): int {
		if ( ! $this->tables_exist() ) {
			return 0;
		}

		$cached = wp_cache_get( 'count_items', self::CACHE_GROUP );
		if ( false !== $cached ) {
			return (int) $cached;
		}

		global $wpdb;
		$rel = $wpdb->prefix . 'fbv_attachment_folder';
		$fbv = $wpdb->prefix . 'fbv';
		// JOIN across FileBird's two custom tables — no WP API exists.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$count = (int) $wpdb->get_var(
			$wpdb->prepare(
				'SELECT COUNT(DISTINCT rel.attachment_id) FROM %i rel INNER JOIN %i fbv ON fbv.id = rel.folder_id AND fbv.type = %d',
				$rel,
				$fbv,
				0
			)
		);

		wp_cache_set( 'count_items', $count, self::CACHE_GROUP, MINUTE_IN_SECONDS );
		return $count;
	}

	/**
	 * Read every folder row (type=0), ordered so BFS sees siblings in the
	 * source's intended order. Cached only for the duration of the import
	 * and explicitly invalidated at the end of run() so a re-import sees
	 * the live source.
	 *
	 * @return array<int, array<string, mixed>>
	 */
	private function fetch_folder_rows(): array {
		$cached = wp_cache_get( 'import_folder_rows', self::CACHE_GROUP );
		if ( false !== $cached ) {
			return is_array( $cached ) ? $cached : array();
		}

		global $wpdb;
		$fbv_table = $wpdb->prefix . 'fbv';
		// Reading FileBird's wp_fbv table — no WP API exists.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				'SELECT id, name, parent, ord FROM %i WHERE type = %d ORDER BY parent ASC, ord ASC, id ASC',
				$fbv_table,
				0
			),
			ARRAY_A
		);

		$rows = is_array( $rows ) ? $rows : array();
		wp_cache_set( 'import_folder_rows', $rows, self::CACHE_GROUP, MINUTE_IN_SECONDS );
		return $rows;
	}

	/**
	 * Read every attachment ↔ folder relationship. Same caching contract
	 * as fetch_folder_rows().
	 *
	 * @return array<int, array<string, mixed>>
	 */
	private function fetch_relations(): array {
		$cached = wp_cache_get( 'import_relations', self::CACHE_GROUP );
		if ( false !== $cached ) {
			return is_array( $cached ) ? $cached : array();
		}

		global $wpdb;
		$rel_table = $wpdb->prefix . 'fbv_attachment_folder';
		// Reading FileBird's wp_fbv_attachment_folder table — no WP API exists.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				'SELECT folder_id, attachment_id FROM %i ORDER BY attachment_id ASC, folder_id ASC',
				$rel_table
			),
			ARRAY_A
		);

		$rows = is_array( $rows ) ? $rows : array();
		wp_cache_set( 'import_relations', $rows, self::CACHE_GROUP, MINUTE_IN_SECONDS );
		return $rows;
	}

	/**
	 * Drop every cache entry written by this class. Called at the end of
	 * run() so a subsequent count or re-import always reads fresh data.
	 */
	private function flush_cache(): void {
		wp_cache_delete( 'tables_exist', self::CACHE_GROUP );
		wp_cache_delete( 'count_folders', self::CACHE_GROUP );
		wp_cache_delete( 'count_items', self::CACHE_GROUP );
		wp_cache_delete( 'import_folder_rows', self::CACHE_GROUP );
		wp_cache_delete( 'import_relations', self::CACHE_GROUP );
	}

	/**
	 * Run the import: rebuild the folder hierarchy and assign attachments.
	 *
	 * @param string $mode "merge" or "replace".
	 * @return array<string, mixed>|WP_Error
	 */
	public function run( string $mode ) {
		if ( ! $this->tables_exist() ) {
			return new WP_Error( 'kookoawpf_importer_no_source', __( 'FileBird tables not found.', 'kookoa-folders' ) );
		}

		$mode = ( 'replace' === $mode ) ? 'replace' : 'merge';

		if ( 'replace' === $mode ) {
			$this->purge_target_taxonomy();
		}

		$rows = $this->fetch_folder_rows();
		if ( empty( $rows ) ) {
			$this->flush_cache();
			return array(
				'folders_created' => 0,
				'items_assigned'  => 0,
				'items_skipped'   => 0,
				'errors'          => array(),
			);
		}

		$shaped = array();
		foreach ( $rows as $row ) {
			$shaped[] = array(
				'id'     => (int) $row['id'],
				'parent' => (int) $row['parent'],
				'name'   => (string) $row['name'],
				'ord'    => (int) $row['ord'],
			);
		}

		$errors          = array();
		$folders_created = 0;

		// Map source ord → KooKoa folder_order termmeta so siblings keep
		// their original order in the sidebar.
		$ord_lookup = array();
		foreach ( $shaped as $row ) {
			$ord_lookup[ $row['id'] ] = $row['ord'];
		}

		$create_term = function ( string $name, int $parent_term_id ) use ( &$folders_created, &$errors ) {
			// skip_order = true: we set folder_order ourselves below
			// from the FileBird "ord" column, so paying for
			// get_max_order() inside create() would be wasted work.
			$result = KookoaWPF_Folders::create( $name, $this->get_target_post_type(), $parent_term_id, array( 'skip_order' => true ) );
			if ( is_wp_error( $result ) ) {
				$errors[] = sprintf(
					/* translators: 1: folder name, 2: error message */
					__( 'Could not create folder "%1$s": %2$s', 'kookoa-folders' ),
					$name,
					$result->get_error_message()
				);
				return null;
			}
			++$folders_created;
			return (int) $result['term_id'];
		};

		$id_map = $this->bfs_create_folders( $shaped, $create_term, $errors );

		foreach ( $id_map as $source_id => $term_id ) {
			if ( isset( $ord_lookup[ $source_id ] ) ) {
				KookoaWPF_Folders::update_meta( $term_id, 'folder_order', $ord_lookup[ $source_id ] );
			}
		}

		$relations = $this->fetch_relations();

		$by_folder = array();
		$seen_att  = array();
		foreach ( $relations as $rel ) {
			$att_id    = (int) $rel['attachment_id'];
			$folder_id = (int) $rel['folder_id'];
			if ( $att_id <= 0 || ! isset( $id_map[ $folder_id ] ) ) {
				continue;
			}
			// A FileBird attachment may be in multiple folders; we honor
			// only the first occurrence so our 1-file-1-folder invariant
			// is preserved.
			if ( isset( $seen_att[ $att_id ] ) ) {
				continue;
			}
			$seen_att[ $att_id ]         = true;
			$target_term                 = $id_map[ $folder_id ];
			$by_folder[ $target_term ]   = isset( $by_folder[ $target_term ] ) ? $by_folder[ $target_term ] : array();
			$by_folder[ $target_term ][] = $att_id;
		}

		$total_assigned = 0;
		$total_skipped  = 0;
		// Defer term-count recalculation until every folder is populated, so a
		// large import pays one count update per term instead of one per
		// wp_set_object_terms() call. Mirrors KookoaWPF_Folders::assign_items().
		wp_defer_term_counting( true );
		foreach ( $by_folder as $term_id => $att_ids ) {
			$summary         = $this->assign_items_to_folder( $att_ids, (int) $term_id, $mode );
			$total_assigned += $summary['assigned'];
			$total_skipped  += $summary['skipped'];
		}
		wp_defer_term_counting( false );

		// Only mark imported when at least one folder was actually
		// created — a "substantial failure" (zero folders despite source
		// rows being present) leaves the importer in a re-runnable
		// state, so the user can retry after fixing whatever blocked it.
		if ( $folders_created > 0 ) {
			$this->mark_imported();
		}
		$this->flush_cache();

		return array(
			'folders_created' => $folders_created,
			'items_assigned'  => $total_assigned,
			'items_skipped'   => $total_skipped,
			'errors'          => $errors,
		);
	}
}
