<?php

namespace Fotogenic_AI;

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

/**
 * Canonical attachment map for accepted upstream jobs.
 *
 * Single-output jobs use the job ID as the key. Campaign Set terminal outputs
 * use deterministic job-prefixed keys so one accepted job can own many outputs.
 */
final class Generate_Output_Map {
	private const MAX_KEY_LENGTH = 255;
	public const COMPLETED_JOBS_KEY = '__completed_jobs';

	public static function campaign_key( string $job_id, string $asset_id, int $order_index ): string {
		$job_id   = Upstream_Identifier::normalize( $job_id );
		$asset_id = Upstream_Identifier::normalize( $asset_id );
		if (
			'' === $job_id
			|| '' === $asset_id
			|| $order_index < 0
			|| $order_index > 999999
		) {
			return '';
		}
		return hash( 'sha256', $job_id ) . '#' . sprintf( '%06d', $order_index ) . '-' . substr( hash( 'sha256', $asset_id ), 0, 16 );
	}

	public static function belongs_to_job( string $map_key, string $job_id ): bool {
		$job_id = Upstream_Identifier::normalize( $job_id );
		return '' !== $job_id && (
			$map_key === $job_id
			|| 1 === preg_match( '/^' . preg_quote( hash( 'sha256', $job_id ), '/' ) . '#\d{6}-[a-f0-9]{16}$/', $map_key )
		);
	}

	public static function job_id_for_key( string $map_key, array $job_ids ): string {
		foreach ( self::normalize_job_ids( $job_ids ) as $job_id ) {
			if ( self::belongs_to_job( $map_key, $job_id ) ) {
				return $job_id;
			}
		}
		return '';
	}

	/**
	 * @param mixed $value Candidate value.
	 * @return array<string, int>
	 */
	public static function normalize( $value, array $job_ids, string $workflow ): array {
		if ( is_string( $value ) ) {
			$decoded = json_decode( $value, true );
			$value   = is_array( $decoded ) ? $decoded : [];
		}
		if ( ! is_array( $value ) ) {
			return [];
		}
		$job_ids = self::normalize_job_ids( $job_ids );
		$map     = [];
		$attachment_ids = [];
		foreach ( $value as $map_key => $attachment_id ) {
			$map_key       = trim( (string) $map_key );
			$attachment_id = absint( $attachment_id );
			if ( '' === $map_key || strlen( $map_key ) > self::MAX_KEY_LENGTH || $attachment_id <= 0 ) {
				return [];
			}
			$job_id = self::job_id_for_key( $map_key, $job_ids );
			$is_campaign_key = '' !== $job_id && $map_key !== $job_id;
			if (
				'' === $job_id
				|| ( 'campaign_set' === $workflow ) !== $is_campaign_key
				|| isset( $attachment_ids[ $attachment_id ] )
			) {
				return [];
			}
			$map[ $map_key ] = $attachment_id;
			$attachment_ids[ $attachment_id ] = true;
		}
		ksort( $map, SORT_NATURAL );
		return $map;
	}

	/** @return array<string, int> */
	public static function for_job( array $map, string $job_id ): array {
		$outputs = [];
		foreach ( $map as $map_key => $attachment_id ) {
			$map_key       = (string) $map_key;
			$attachment_id = absint( $attachment_id );
			if ( $attachment_id > 0 && self::belongs_to_job( $map_key, $job_id ) ) {
				$outputs[ $map_key ] = $attachment_id;
			}
		}
		ksort( $outputs, SORT_NATURAL );
		return $outputs;
	}

	public static function has_job_outputs( array $map, string $job_id ): bool {
		return ! empty( self::for_job( $map, $job_id ) );
	}

	/** @return int[] */
	public static function attachment_ids( array $map ): array {
		return array_values( array_unique( array_filter( array_map( 'absint', array_values( $map ) ) ) ) );
	}

	/** @return array<string, int> */
	public static function replace_job_slice( array $current, string $job_id, array $incoming, array $job_ids, string $workflow ): array {
		$normalized_current  = self::normalize( $current, $job_ids, $workflow );
		$normalized_incoming = self::normalize( $incoming, $job_ids, $workflow );
		foreach ( array_keys( $normalized_current ) as $map_key ) {
			if ( self::belongs_to_job( (string) $map_key, $job_id ) ) {
				unset( $normalized_current[ $map_key ] );
			}
		}
		return self::normalize( array_merge( $normalized_current, $normalized_incoming ), $job_ids, $workflow );
	}

	/**
	 * @param mixed $value Candidate value.
	 * @return string[]
	 */
	public static function completed_job_ids_from_meta( $value, array $job_ids ): array {
		if ( is_string( $value ) ) {
			$decoded = json_decode( $value, true );
			$value   = is_array( $decoded ) ? $decoded : [];
		}
		$completed = is_array( $value ) ? ( $value[ self::COMPLETED_JOBS_KEY ] ?? [] ) : [];
		if ( ! is_array( $completed ) ) {
			return [];
		}
		return array_values( array_intersect( self::normalize_job_ids( $completed ), self::normalize_job_ids( $job_ids ) ) );
	}

	public static function with_completed_job_ids( array $meta, array $completed_job_ids, array $job_ids ): array {
		$completed_job_ids = array_values( array_intersect( self::normalize_job_ids( $completed_job_ids ), self::normalize_job_ids( $job_ids ) ) );
		if ( empty( $completed_job_ids ) ) {
			unset( $meta[ self::COMPLETED_JOBS_KEY ] );
		} else {
			$meta[ self::COMPLETED_JOBS_KEY ] = $completed_job_ids;
		}
		return $meta;
	}

	/** @return string[] */
	private static function normalize_job_ids( array $job_ids ): array {
		$normalized = [];
		foreach ( $job_ids as $job_id ) {
			$job_id = Upstream_Identifier::normalize( $job_id );
			if ( '' !== $job_id ) {
				$normalized[] = $job_id;
			}
		}
		return array_values( array_unique( $normalized ) );
	}
}
