<?php
/**
 * Generator numerów faktur — obsługuje WIELE szablonów numeracji z jednym domyślnym.
 *
 * Każdy schemat ma `kind`: 'sale' (faktura sprzedaży) lub 'correction' (faktura korygująca).
 * Jeden schemat per `kind` jest domyślny — używany przy wystawianiu danego typu faktury.
 *
 * Szablon używa placeholders:
 *   {Y} rok 4-cyfr · {y} rok 2-cyfr · {m} miesiąc · {d} dzień
 *   {n} counter (padding domyślny) · {n:N} counter z paddingiem N · {prefix} prefix
 *
 * Reset countera per szablon: 'never' | 'yearly' | 'monthly' (domyślnie monthly).
 * Schematy trzymane w opcji `ksef_numbering_schemes` (JSON). Counter per (schemat, okres).
 *
 * @package KSEF\WordPress\V2
 */

declare(strict_types=1);

namespace KSEF\WordPress\V2;

final class Invoice_Numbering {

	// Legacy single-option keys (zachowane dla kompatybilności / migracji)
	public const OPT_TEMPLATE        = 'ksef_invoice_template';
	public const OPT_RESET_PERIOD    = 'ksef_invoice_reset_period';
	public const OPT_COUNTER_PADDING = 'ksef_invoice_counter_padding';
	public const OPT_PREFIX          = 'ksef_invoice_prefix';

	// Nowa opcja — lista schematów numeracji
	public const OPT_SCHEMES = 'ksef_numbering_schemes';

	public const DEFAULT_TEMPLATE     = 'FV/{Y}/{m}/{n}';
	public const DEFAULT_TEMPLATE_KOR = 'KOR/{Y}/{m}/{n}';

	public const KINDS = [ 'sale', 'correction' ];

	/**
	 * Zwraca listę schematów numeracji. Gdy brak — buduje domyślne (sprzedaż + korekta).
	 *
	 * @param string|null $kind  null = wszystkie; 'sale'|'correction' = filtr.
	 * @return array<int,array<string,mixed>>
	 */
	public static function get_schemes( ?string $kind = null ): array {
		$raw = get_option( self::OPT_SCHEMES, '' );
		$schemes = is_string( $raw ) && '' !== $raw ? json_decode( $raw, true ) : ( is_array( $raw ) ? $raw : [] );
		if ( ! is_array( $schemes ) || empty( $schemes ) ) {
			// Migracja: zbuduj domyślny schemat sprzedaży z legacy opcji + schemat korekty.
			$schemes = [
				[
					'id'           => 'default',
					'name'         => __( 'Domyślna', 'prostafaktura-ksef' ),
					'kind'         => 'sale',
					'template'     => '' !== trim( (string) get_option( self::OPT_TEMPLATE, '' ) ) ? (string) get_option( self::OPT_TEMPLATE, self::DEFAULT_TEMPLATE ) : self::DEFAULT_TEMPLATE,
					'prefix'       => (string) get_option( self::OPT_PREFIX, '' ),
					'reset_period' => (string) get_option( self::OPT_RESET_PERIOD, 'monthly' ),
					'padding'      => max( 1, min( 10, (int) get_option( self::OPT_COUNTER_PADDING, 4 ) ) ),
					'is_default'   => true,
				],
				[
					'id'           => 'default_kor',
					'name'         => __( 'Korygująca', 'prostafaktura-ksef' ),
					'kind'         => 'correction',
					'template'     => self::DEFAULT_TEMPLATE_KOR,
					'prefix'       => '',
					'reset_period' => 'monthly',
					'padding'      => 4,
					'is_default'   => true,
				],
			];
		}
		// Normalizacja: kind + dokładnie jeden domyślny per kind.
		$seen_default = [];
		foreach ( $schemes as &$s ) {
			$k = in_array( $s['kind'] ?? '', self::KINDS, true ) ? $s['kind'] : 'sale';
			$s['kind'] = $k;
			if ( ! empty( $s['is_default'] ) && empty( $seen_default[ $k ] ) ) {
				$seen_default[ $k ] = true;
				$s['is_default'] = true;
			} else {
				$s['is_default'] = false;
			}
		}
		unset( $s );
		// Gdy dla danego kind brak domyślnego — ustaw pierwszy.
		foreach ( self::KINDS as $k ) {
			if ( empty( $seen_default[ $k ] ) ) {
				foreach ( $schemes as &$s2 ) {
					if ( $s2['kind'] === $k ) {
						$s2['is_default'] = true;
						break;
					}
				}
				unset( $s2 );
			}
		}
		$schemes = array_values( $schemes );
		if ( null !== $kind ) {
			return array_values( array_filter( $schemes, static fn ( $s ) => ( $s['kind'] ?? 'sale' ) === $kind ) );
		}
		return $schemes;
	}

	/**
	 * Zapisuje listę schematów.
	 *
	 * @param array<int,array<string,mixed>> $schemes
	 */
	public static function save_schemes( array $schemes ): void {
		$clean = [];
		$seen_default = [];
		foreach ( $schemes as $i => $s ) {
			$id   = sanitize_key( (string) ( $s['id'] ?? '' ) ) ?: ( 'scheme_' . ( $i + 1 ) );
			$kind = in_array( $s['kind'] ?? '', self::KINDS, true ) ? $s['kind'] : 'sale';
			$is_default = ! empty( $s['is_default'] ) && empty( $seen_default[ $kind ] );
			if ( $is_default ) {
				$seen_default[ $kind ] = true;
			}
			$default_tpl = 'correction' === $kind ? self::DEFAULT_TEMPLATE_KOR : self::DEFAULT_TEMPLATE;
			$clean[] = [
				'id'           => $id,
				'name'         => sanitize_text_field( (string) ( $s['name'] ?? $id ) ),
				'kind'         => $kind,
				'template'     => sanitize_text_field( (string) ( $s['template'] ?? $default_tpl ) ),
				'prefix'       => sanitize_text_field( (string) ( $s['prefix'] ?? '' ) ),
				'reset_period' => in_array( $s['reset_period'] ?? '', [ 'never', 'yearly', 'monthly' ], true ) ? $s['reset_period'] : 'monthly',
				'padding'      => max( 1, min( 10, (int) ( $s['padding'] ?? 4 ) ) ),
				'is_default'   => $is_default,
			];
		}
		// Gdy dla danego kind brak domyślnego — ustaw pierwszy tego typu.
		foreach ( self::KINDS as $k ) {
			if ( empty( $seen_default[ $k ] ) ) {
				foreach ( $clean as &$c ) {
					if ( $c['kind'] === $k ) {
						$c['is_default'] = true;
						break;
					}
				}
				unset( $c );
			}
		}
		update_option( self::OPT_SCHEMES, wp_json_encode( $clean ), false );
	}

	/**
	 * Zwraca domyślny schemat numeracji dla danego typu faktury.
	 *
	 * @param string $kind 'sale' | 'correction'
	 * @return array<string,mixed>
	 */
	public static function get_default_scheme( string $kind = 'sale' ): array {
		$kind = in_array( $kind, self::KINDS, true ) ? $kind : 'sale';
		foreach ( self::get_schemes() as $s ) {
			if ( ( $s['kind'] ?? 'sale' ) === $kind && ! empty( $s['is_default'] ) ) {
				return $s;
			}
		}
		foreach ( self::get_schemes() as $s ) {
			if ( ( $s['kind'] ?? 'sale' ) === $kind ) {
				return $s;
			}
		}
		return [
			'id'           => 'correction' === $kind ? 'default_kor' : 'default',
			'kind'         => $kind,
			'template'     => 'correction' === $kind ? self::DEFAULT_TEMPLATE_KOR : self::DEFAULT_TEMPLATE,
			'prefix'       => '',
			'reset_period' => 'monthly',
			'padding'      => 4,
		];
	}

	/**
	 * Generuje kolejny numer faktury wg domyślnego (lub wskazanego) schematu.
	 *
	 * @param string|null $template_or_scheme_id  null = domyślny schemat danego typu;
	 *        gdy podany string pasujący do id schematu — użyj go; inaczej traktuj jako surowy szablon.
	 * @param string      $kind  'sale' | 'correction' — typ faktury (dla wyboru domyślnego schematu).
	 */
	public static function next( ?string $template_or_scheme_id = null, string $kind = 'sale' ): string {
		$scheme = self::resolve_scheme( $template_or_scheme_id, $kind );
		// Zabezpieczenie: pusty szablon (np. wyczyszczona opcja) → domyślny, by numer nigdy nie był pusty.
		if ( '' === trim( (string) ( $scheme['template'] ?? '' ) ) ) {
			$scheme['template'] = 'correction' === $kind ? self::DEFAULT_TEMPLATE_KOR : self::DEFAULT_TEMPLATE;
		}

		$now = time();
		$Y = gmdate( 'Y', $now ); $y = gmdate( 'y', $now );
		$m = gmdate( 'm', $now ); $d = gmdate( 'd', $now );

		$counter_key = self::counter_key( (string) $scheme['id'], self::effective_reset( (string) $scheme['reset_period'], (string) $scheme['template'] ), $Y, $m );
		$counter     = (int) get_option( $counter_key, 0 ) + 1;
		update_option( $counter_key, $counter, false );

		return self::format( (string) $scheme['template'], (string) $scheme['prefix'], (int) $scheme['padding'], $counter, $Y, $y, $m, $d );
	}

	/**
	 * Preview — bez inkrementacji.
	 */
	public static function preview( ?string $template_or_scheme_id = null, int $sample_counter = 1, string $kind = 'sale' ): string {
		$scheme = self::resolve_scheme( $template_or_scheme_id, $kind );
		$now = time();
		return self::format(
			(string) $scheme['template'], (string) $scheme['prefix'], (int) $scheme['padding'],
			$sample_counter, gmdate( 'Y', $now ), gmdate( 'y', $now ), gmdate( 'm', $now ), gmdate( 'd', $now )
		);
	}

	/**
	 * Aktualny KOLEJNY numer (licznik+1) dla schematu w bieżącym okresie — do podglądu/edycji w UI.
	 *
	 * @param array<string,mixed> $scheme
	 */
	public static function current_next( array $scheme ): int {
		$now = time();
		$key = self::counter_key(
			(string) ( $scheme['id'] ?? 'default' ),
			self::effective_reset( (string) ( $scheme['reset_period'] ?? 'monthly' ), (string) ( $scheme['template'] ?? '' ) ),
			gmdate( 'Y', $now ),
			gmdate( 'm', $now )
		);
		return (int) get_option( $key, 0 ) + 1;
	}

	/**
	 * Ustawia licznik tak, by NASTĘPNY wygenerowany numer wynosił $next (w bieżącym okresie).
	 *
	 * @param array<string,mixed> $scheme
	 */
	public static function set_next( array $scheme, int $next ): void {
		$next = max( 1, $next );
		$now  = time();
		$key  = self::counter_key(
			(string) ( $scheme['id'] ?? 'default' ),
			self::effective_reset( (string) ( $scheme['reset_period'] ?? 'monthly' ), (string) ( $scheme['template'] ?? '' ) ),
			gmdate( 'Y', $now ),
			gmdate( 'm', $now )
		);
		update_option( $key, $next - 1, false );
	}

	/**
	 * @return array<string,mixed>
	 */
	private static function resolve_scheme( ?string $arg, string $kind = 'sale' ): array {
		if ( null === $arg || '' === $arg ) {
			return self::get_default_scheme( $kind );
		}
		// dopasuj po id
		foreach ( self::get_schemes() as $s ) {
			if ( (string) $s['id'] === $arg ) {
				return $s;
			}
		}
		// inaczej — surowy szablon (legacy)
		$d = self::get_default_scheme( $kind );
		$d['template'] = $arg;
		return $d;
	}

	private static function format( string $template, string $prefix, int $padding, int $counter, string $Y, string $y, string $m, string $d ): string {
		$result = strtr( $template, [
			'{Y}' => $Y, '{y}' => $y, '{m}' => $m, '{d}' => $d, '{prefix}' => $prefix,
		] );
		$result = (string) preg_replace_callback(
			'/\{n:(\d+)\}/',
			static fn ( $mm ) => str_pad( (string) $counter, max( 1, (int) $mm[1] ), '0', STR_PAD_LEFT ),
			$result
		);
		return str_replace( '{n}', str_pad( (string) $counter, max( 1, $padding ), '0', STR_PAD_LEFT ), $result );
	}

	/**
	 * Bezpieczny okres resetu licznika. Licznik MOŻE zostać zresetowany tylko jeśli szablon
	 * zawiera token odpowiadający okresowi — inaczej numery powtarzałyby się między okresami
	 * (np. reset miesięczny przy szablonie bez {m} → ten sam numer co miesiąc). Gwarancja unikalności.
	 */
	public static function effective_reset( string $reset, string $template ): string {
		$has_month = false !== strpos( $template, '{m}' ) || false !== strpos( $template, '{d}' );
		$has_year  = false !== strpos( $template, '{Y}' ) || false !== strpos( $template, '{y}' );
		if ( 'monthly' === $reset && ! $has_month ) {
			return $has_year ? 'yearly' : 'never';
		}
		if ( 'yearly' === $reset && ! $has_year ) {
			return 'never';
		}
		return $reset;
	}

	private static function counter_key( string $scheme_id, string $reset, string $year, string $month ): string {
		$base = 'ksef_invc_' . $scheme_id . '_';
		return match ( $reset ) {
			'monthly' => $base . $year . '_' . $month,
			'never'   => $base . 'global',
			default   => $base . $year,
		};
	}
}
