<?php
namespace Stnf\StocktitanNewsFeed;

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

final class Plugin {
	private static $instance;

	public static function get_instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	private function __construct() {

		// Register assets first so handles exist when the block is registered
		add_action( 'init', array( $this, 'register_assets' ), 9 );
		add_action( 'init', array( $this, 'register' ), 10 );
	}

	/**
	 * Register frontend assets. Uses minified files when available, keeping originals intact.
	 */
	public function register_assets() {
		$base_path = trailingslashit( STNF_PATH . 'assets' );
		$base_url  = trailingslashit( STNF_URL . 'assets' );

		// Style (frontend/editor): prefer .min.css if present
		$style_file = file_exists( $base_path . 'style.min.css' ) ? 'style.min.css' : 'style.css';
		if ( file_exists( $base_path . $style_file ) ) {
			$ver = (string) filemtime( $base_path . $style_file );
			wp_register_style( 'stnf-style', $base_url . $style_file, array(), $ver );
		}

		// View script (frontend behavior only): prefer .min.js if present
		$view_file = file_exists( $base_path . 'view.min.js' ) ? 'view.min.js' : 'view.js';
		if ( file_exists( $base_path . $view_file ) ) {
			$ver = (string) filemtime( $base_path . $view_file );
			wp_register_script( 'stnf-view', $base_url . $view_file, array(), $ver, true ); // in footer
		}
	}
	public function register() {
		$args = array(
			'render_callback' => array( $this, 'render' ),
		);

		// If our custom handles exist, use them to favor minified assets on the frontend
		if ( wp_style_is( 'stnf-style', 'registered' ) ) {
			$args['style'] = 'stnf-style';
		}
		if ( wp_script_is( 'stnf-view', 'registered' ) ) {
			$args['view_script'] = 'stnf-view';
		}

		register_block_type( STNF_PATH . 'block.json', $args );

		if ( wp_script_is( 'stocktitan-news-feed-news-feed-editor-script', 'registered' ) ) {
			wp_set_script_translations(
				'stocktitan-news-feed-news-feed-editor-script',
				'stocktitan-news-feed-block',
				STNF_PATH . 'languages'
			);
		}
	}

	/** ====== DATA LAYER =================================================== */

	/**
	 * Fetch news items with ETag-based persistent caching.
	 *
	 * Strategy:
	 * - Data is stored in a WordPress transient for 2 minutes.
	 * - When the transient expires, a conditional GET (If-None-Match) is sent.
	 *   • HTTP 304 → server data unchanged: refresh the transient TTL, return cached items.
	 *   • HTTP 200 → new data: parse, store new ETag + items, return items.
	 * - On any network error, stale cache is returned (graceful degradation).
	 *
	 * @return array Parsed news items.
	 */
	private function fetch_news_items() {
		// Per-request in-memory cache: avoid redundant transient reads on the same page load.
		static $request_cache = null;
		if ( null !== $request_cache ) {
			return $request_cache;
		}

		$token = defined( 'STNF_API_TOKEN' ) ? trim( (string) STNF_API_TOKEN ) : '';
		if ( '' === $token ) {
			$request_cache = array();
			return $request_cache;
		}

		$transient_key = 'stnf_news_cache_v1';
		// Transient payload contains the latest ETag plus parsed items.
			$cached = get_transient( $transient_key );

		$base = esc_url_raw( STNF_API_BASE );
		parse_str( (string) wp_parse_url( $base, PHP_URL_QUERY ), $query_args );
		$url = empty( $query_args['token'] ) ? add_query_arg( array( 'token' => $token ), $base ) : $base;

		$args = array(
			'timeout'   => 10,
			'sslverify' => true,
			'headers'   => array( 'Accept' => 'application/json' ),
		);

		// Conditional GET: if we have a cached ETag, let the server tell us if data changed.
		if ( is_array( $cached ) && ! empty( $cached['etag'] ) ) {
			$args['headers']['If-None-Match'] = $cached['etag'];
		}

		$response = wp_remote_get( $url, $args );
		if ( is_wp_error( $response ) ) {
			// Network error: return stale cache if available so the page still renders.
			$request_cache = is_array( $cached ) ? $cached['items'] : array();
			return $request_cache;
		}

		$code = (int) wp_remote_retrieve_response_code( $response );

		// 304 Not Modified: data unchanged, just refresh the transient TTL.
		if ( 304 === $code && is_array( $cached ) ) {
			set_transient( $transient_key, $cached, 2 * MINUTE_IN_SECONDS );
			$request_cache = $cached['items'];
			return $request_cache;
		}

		if ( 200 !== $code ) {
			$request_cache = is_array( $cached ) ? $cached['items'] : array();
			return $request_cache;
		}

		$body = wp_remote_retrieve_body( $response );
		if ( '' === $body ) {
			$request_cache = is_array( $cached ) ? $cached['items'] : array();
			return $request_cache;
		}

		$json = json_decode( $body, true );
		if ( ! is_array( $json ) ) {
			$request_cache = is_array( $cached ) ? $cached['items'] : array();
			return $request_cache;
		}

		$out = array();
		foreach ( $json as $row ) {
			$news         = isset( $row['news'] ) && is_array( $row['news'] ) ? $row['news'] : array();
			$title        = isset( $news['title'] ) ? wp_strip_all_tags( (string) $news['title'] ) : '';
			$link         = isset( $news['url'] ) ? esc_url_raw( (string) $news['url'] ) : '';
			$date         = isset( $news['date'] ) ? (string) $news['date'] : '';
			$symbol_raw   = isset( $news['symbol'] ) ? (string) $news['symbol'] : '';
			$exchange_raw = isset( $news['exchange'] ) ? (string) $news['exchange'] : '';
			$symbol       = $this->first_symbol( $symbol_raw, true );
			$exchange     = $this->first_exchange( $exchange_raw );
			$tag          = isset( $news['tag'] ) ? sanitize_text_field( (string) $news['tag'] ) : '';

			$summary = '';
			if ( isset( $news['summary'] ) && is_string( $news['summary'] ) ) {
				$summary_raw = trim( $news['summary'] );
				if ( '' !== $summary_raw ) {
					$decoded = json_decode( $summary_raw, true );
					if ( JSON_ERROR_NONE === json_last_error() && is_array( $decoded ) && isset( $decoded['summary'] ) ) {
						$summary = wp_strip_all_tags( (string) $decoded['summary'] );
					} else {
						$summary = wp_strip_all_tags( $summary_raw );
					}
				}
			}

			// Skip malformed rows that would otherwise render a blank headline item.
			if ( '' === $title ) {
				continue;
			}

			$out[] = array(
				'title'    => $title,
				'link'     => $link,
				'date'     => $date,
				'symbol'   => $symbol,
				'exchange' => $exchange,
				'summary'  => $summary,
				'tag'      => $tag,
			);
		}

		$etag  = wp_remote_retrieve_header( $response, 'etag' );
		$entry = array(
			'etag'  => $etag,
			'items' => $out,
		);
		set_transient( $transient_key, $entry, 2 * MINUTE_IN_SECONDS );

		$request_cache = $out;
		return $request_cache;
	}

	/** ====== RENDER ======================================================= */

	public function render( $attributes ) {
		$defaults = array(
			'containerID'        => 'stockTitanNewsFeed',

			'colorBackground'    => '',
			'colorDate'          => '',
			'colorSymbol'        => '',
			'colorExchange'      => '',
			'colorTitle'         => '',
			'colorTag'           => '',
			'colorTagBackground' => '',
			'colorSummary'       => '',
			'colorDivider'       => '',

			'showLogo'           => true,
			'showDate'           => true,
			'showSummary'        => true,
			'showTag'            => true,
			'showAttribution'    => false,
			'maxItems'           => 10,
			'dateFormat'         => 'date',
			'preset'             => 'theme',
		);

		$attributes       = wp_parse_args( (array) $attributes, $defaults );
		$raw_container_id = isset( $attributes['containerID'] ) ? (string) $attributes['containerID'] : '';
		$container_id     = $this->sanitize_container_id( $raw_container_id );
		$default_id       = $this->sanitize_container_id( 'stockTitanNewsFeed' );
		if ( '' === $container_id || $container_id === $default_id ) {
			$container_id = wp_unique_id( 'stnf-' );
		}
		$max_items    = max( 1, min( 50, absint( $attributes['maxItems'] ) ) );
		$show_logo    = (bool) $attributes['showLogo'];
		$show_date    = (bool) $attributes['showDate'];
		$show_summary = (bool) $attributes['showSummary'];
		$show_tag     = (bool) $attributes['showTag'];
		// Validate date format (no legacy mappings needed)
		$raw_mode    = isset( $attributes['dateFormat'] ) ? (string) $attributes['dateFormat'] : '';
		$date_format = in_array( $raw_mode, array( 'date', 'utc', 'ny', 'local' ), true ) ? $raw_mode : 'date';
		$preset      = in_array( $attributes['preset'], array( 'theme', 'stocktitan-dark', 'nyse', 'nasdaq', 'bloomberg', 'ft' ), true ) ? $attributes['preset'] : 'theme';
		$classes     = array( 'stocktitan-news-feed', 'stnf', 'stnf--preset-' . $preset );
		if ( ! $show_logo ) {
			$classes[] = 'stnf--no-logo';
		}

		// Sanitize colors (empty string = inherit theme)
		$c_bg       = $this->sanitize_color( $attributes['colorBackground'] );
		$c_title    = $this->sanitize_color( $attributes['colorTitle'] );
		$c_summary  = $this->sanitize_color( $attributes['colorSummary'] );
		$c_date     = $this->sanitize_color( $attributes['colorDate'] );
		$c_symbol   = $this->sanitize_color( $attributes['colorSymbol'] );
		$c_exchange = $this->sanitize_color( $attributes['colorExchange'] );
		$c_tag      = $this->sanitize_color( $attributes['colorTag'] );
		$c_tag_bg   = $this->sanitize_color( $attributes['colorTagBackground'] );
		$c_divider  = $this->sanitize_color( $attributes['colorDivider'] );

		$style_vars = array();
		if ( $c_bg ) {
			$style_vars[] = '--stnf-bg:' . $c_bg;
		}
		if ( $c_title ) {
			$style_vars[] = '--stnf-title:' . $c_title;
		}
		if ( $c_summary ) {
			$style_vars[] = '--stnf-summary:' . $c_summary;
		}
		if ( $c_date ) {
			$style_vars[] = '--stnf-date:' . $c_date;
		}
		if ( $c_symbol ) {
			$style_vars[] = '--stnf-symbol:' . $c_symbol;
		}
		if ( $c_exchange ) {
			$style_vars[] = '--stnf-exchange:' . $c_exchange;
		}
		if ( $c_tag ) {
			$style_vars[] = '--stnf-tag:' . $c_tag;
		}
		if ( $c_tag_bg ) {
			$style_vars[] = '--stnf-tag-bg:' . $c_tag_bg;
		}
		if ( $c_divider ) {
			$style_vars[] = '--stnf-divider:' . $c_divider;
		}
		$style_attr = $style_vars ? ' style="' . esc_attr( implode( ';', $style_vars ) ) . '"' : '';

		$items = $this->fetch_news_items();
		if ( empty( $items ) ) {
			return '<div class="stocktitan-news-feed"' . $style_attr . '>' . esc_html__( 'No news available.', 'stocktitan-news-feed-block' ) . '</div>';
		}

		// Capture WP date/time formats so JS can mimic them client-side
		$wp_df = (string) get_option( 'date_format' );
		$wp_tf = (string) get_option( 'time_format' );

		$container_attr = $container_id ? ' id="' . esc_attr( $container_id ) . '"' : '';
		$out            = '<div' . $container_attr . ' class="' . esc_attr( $this->esc_classes( $classes ) ) . '"' . $style_attr . '>';
		$out           .= '<ul class="stnf-list" role="list">';

		$i = 0;
		foreach ( $items as $it ) {
			if ( $i++ >= $max_items ) {
				break;
			}

			$title    = $it['title'];
			$linkurl  = $it['link'];
			$symbol   = $it['symbol'];
			$exchange = $it['exchange'];
			$summary  = $it['summary'];
			$tags_raw = isset( $it['tag'] ) ? (string) $it['tag'] : ( isset( $it['tags'] ) ? (string) $it['tags'] : '' );

			$date_display = '';
			$time_attrs   = '';
			if ( $show_date && ! empty( $it['date'] ) ) {
				$date_display = $this->format_date( $it['date'], $date_format );
				if ( 'local' === $date_format ) {
					$time_attrs = ' data-localize="1" data-df="' . esc_attr( $wp_df ) . '" data-tf="' . esc_attr( $wp_tf ) . '"';
				}
			}

			$out .= '<li class="stnf-item">';

			// ---------- HEADER: logo (optional) + title + meta (symbol/exchange/date) ----------
			$out .= '<div class="stnf-head">';

			if ( $show_logo && $symbol ) {
				$logo_url = $this->symbol_logo_url( $symbol );        // https://static.stocktitan.net/company-logo/nvda-sm.png
				$news_url = $this->symbol_news_url( $symbol );        // https://www.stocktitan.net/news/NVDA/
				if ( $logo_url ) {
					/* translators: %s: Stock symbol (e.g., NVDA) */
					$aria_label = sprintf( __( 'News for %s', 'stocktitan-news-feed-block' ), $symbol );
					$out       .= '<a class="stnf-logo" href="' . esc_url( $news_url ) . '" target="_blank" rel="nofollow noopener" aria-label="' . esc_attr( $aria_label ) . '">';
					$out       .= '<img src="' . esc_url( $logo_url ) . '" alt="' . esc_attr( $symbol . ' logo' ) . '" loading="lazy" decoding="async" width="56" height="56" />';
					$out       .= '</a>';
				}
			}

			$out .= '<div class="stnf-head-main">';

			// Title
			if ( $linkurl ) {
				$out .= '<a class="stnf-title" href="' . esc_url( $linkurl ) . '" rel="nofollow noopener" target="_blank">' . esc_html( $title ) . '</a>';
			} else {
				$out .= '<span class="stnf-title">' . esc_html( $title ) . '</span>';
			}

			$out .= '</div>'; // .stnf-head-main
			$out .= '</div>'; // .stnf-head

			// ---------- Body (summary + tags) ----------

			$meta = array();

			if ( $symbol ) {
				$symbol_url = $this->symbol_overview_url( $symbol ); // symbol -> overview page (as before)
				if ( $symbol_url ) {
					/* translators: %s: Stock symbol (e.g., NVDA) */
					$aria_label_view = sprintf( __( 'View %s on StockTitan', 'stocktitan-news-feed-block' ), $symbol );
					$meta[]          = '<a class="stnf-symbol" href="' . esc_url( $symbol_url ) . '" target="_blank" rel="nofollow noopener" aria-label="' . esc_attr( $aria_label_view ) . '">' . esc_html( $symbol ) . '</a>';
				} else {
					$meta[] = '<span class="stnf-symbol">' . esc_html( $symbol ) . '</span>';
				}
			}

			if ( $exchange ) {
				$meta[] = '<span class="stnf-exchange">' . esc_html( $exchange ) . '</span>';
			}

			if ( $date_display ) {
				$suffix = '';
				if ( 'utc' === $date_format ) {
					$suffix = __( 'UTC', 'stocktitan-news-feed-block' );
				} elseif ( 'ny' === $date_format ) {
					$suffix = __( 'MARKET', 'stocktitan-news-feed-block' );
				} elseif ( 'local' === $date_format ) {
					$suffix = __( 'LOCAL', 'stocktitan-news-feed-block' );
				}

				$content = '<span class="stnf-date-txt">' . esc_html( $date_display ) . '</span>';
				if ( $suffix ) {
					$content .= ' <span class="stnf-tz">' . esc_html( $suffix ) . '</span>';
				}

				$meta[] = '<time class="stnf-date" datetime="' . esc_attr( $it['date'] ) . '"' . $time_attrs . '>' . $content . '</time>';
			}

			if ( $meta ) {
				$out .= '<div class="stnf-meta">' . implode( ' <span class="stnf-sep" aria-hidden="true">&bull;</span> ', $meta ) . '</div>';
			}
			if ( $show_summary && $summary ) {
				$out .= '<div class="stnf-summary">' . esc_html( $summary ) . '</div>';
			}

			if ( $show_tag && $tags_raw ) {
				$tags = array_filter( array_map( 'trim', explode( ',', $tags_raw ) ) );
				if ( $tags ) {
					$out .= '<div class="stnf-tags">';
					foreach ( $tags as $tg ) {
						$out .= '<span class="stnf-tag">#' . esc_html( $tg ) . '</span>';
					}
					$out .= '</div>';
				}
			}

			$out .= '</li>';
		}

		$out .= '</ul>';
		// Optional attribution (Guideline 10: default off; opt-in only)
		if ( ! empty( $attributes['showAttribution'] ) ) {
			$live_url = 'https://www.stocktitan.net/news/live.html';
			$out     .= '<div class="stnf-footer">'
					. '<a href="' . esc_url( $live_url ) . '" target="_blank" rel="nofollow noopener">' . esc_html__( 'Powered by StockTitan', 'stocktitan-news-feed-block' ) . '</a>'
					. '</div>';
		}
		$out .= '</div>';
		return $out;
	}

	/** ====== HELPERS ====================================================== */

	private function sanitize_color( $color ) {
		$color = trim( (string) $color );
		if ( '' === $color ) {
			return '';
		}
		// Hex short or full.
		if ( preg_match( '/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/', $color ) ) {
			return $color;
		}
		// Named.
		if ( 'transparent' === strtolower( $color ) ) {
			return $color;
		}
		// rgb() / rgba(): only digits, spaces, commas, dots, and an optional alpha.
		if ( preg_match( '/^rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}(\s*,\s*[\d.]+)?\s*\)$/', $color ) ) {
			return $color;
		}
		// hsl() / hsla(): degrees, percentages, and an optional alpha.
		if ( preg_match( '/^hsla?\(\s*[\d.]+\s*,\s*[\d.]+%\s*,\s*[\d.]+%(\s*,\s*[\d.]+)?\s*\)$/', $color ) ) {
			return $color;
		}
		return '';
	}

	private function sanitize_container_id( $id ) {
		$id = strtolower( (string) $id );
		$id = preg_replace( '/[^a-z0-9\-_]/', '-', $id );
		$id = trim( $id, '-' );
		return $id;
	}

	private function format_date( $iso, $mode ) {
		$ts = strtotime( $iso );
		if ( ! $ts ) {
			return '';
		}
		$df = (string) get_option( 'date_format' );
		$tf = (string) get_option( 'time_format' );
		// Date only
		if ( 'date' === $mode ) {
			return wp_date( $df, $ts );
		}
		// Human-friendly UTC
		if ( 'utc' === $mode ) {
			$fmt = trim( $df . ' ' . $tf );
			return wp_date( $fmt, $ts, new \DateTimeZone( 'UTC' ) );
		}
		// New York (Market hours)
		if ( 'ny' === $mode ) {
			$fmt = trim( $df . ' ' . $tf );
			return wp_date( $fmt, $ts, new \DateTimeZone( 'America/New_York' ) );
		}
		// Local (browser) -> provide a server fallback (site tz), JS will localize client-side
		if ( 'local' === $mode ) {
			return wp_date( $df . ' ' . $tf, $ts );
		}
		// Default = date only
		return wp_date( $df, $ts );
	}

	/** Stock overview page (symbol link in meta). */
	private function symbol_overview_url( $symbol ) {
		$symbol = $this->sanitize_symbol( $symbol, true );
		if ( '' === $symbol ) {
			return '';
		}
		$base = 'https://www.stocktitan.net/overview/';
		return trailingslashit( $base . rawurlencode( $symbol ) );
	}

	/** Stock news page (logo link). */
	private function symbol_news_url( $symbol ) {
		$symbol = $this->sanitize_symbol( $symbol, true ); // UPPERCASE
		if ( '' === $symbol ) {
			return '';
		}
		$base = 'https://www.stocktitan.net/news/';
		return trailingslashit( $base . rawurlencode( $symbol ) );
	}

	/** Company logo asset (lowercase symbol). */
	private function symbol_logo_url( $symbol ) {
		$lc = $this->sanitize_symbol( $symbol, false ); // lowercase
		if ( '' === $lc ) {
			return '';
		}
		return 'https://static.stocktitan.net/company-logo/' . rawurlencode( $lc ) . '-sm.png';
	}

	/**
	 * Sanitize a stock symbol. Keeps letters, digits, dot, hyphen.
	 * @param string $symbol
	 * @param bool   $upper If true, return UPPERCASE; else lowercase.
	 */
	private function sanitize_symbol( $symbol, $upper = true ) {
		$s = preg_replace( '/[^A-Za-z0-9\.\-]/', '', (string) $symbol );
		$s = $upper ? strtoupper( $s ) : strtolower( $s );
		return $s;
	}

	/**
	 * Return the first symbol from a comma-separated list, sanitized.
	 * Examples: "NWSA,NWS" => "NWSA", "BRK.B,BRK" => "BRK.B"
	 * @param string $symbols Raw symbol string from API.
	 * @param bool   $upper   True for UPPERCASE (typical for display/links), false for lowercase (e.g. logo path needs lowercase).
	 */
	private function first_symbol( $symbols, $upper = true ) {
		$symbols = (string) $symbols;
		if ( '' === $symbols ) {
			return '';
		}
		// Split on commas, allow spaces around commas.
		$parts = preg_split( '/\s*,\s*/', $symbols, 2 );
		$first = isset( $parts[0] ) ? $parts[0] : '';
		return $this->sanitize_symbol( $first, $upper );
	}

	/**
	 * Return the first exchange/market from a comma-separated list.
	 * Examples: "NYSE,NASDAQ" => "NYSE"; trims whitespace and sanitizes.
	 * @param string $exchanges Raw exchange string from API.
	 */
	private function first_exchange( $exchanges ) {
		$exchanges = (string) $exchanges;
		if ( '' === $exchanges ) {
			return '';
		}
		$parts = preg_split( '/\s*,\s*/', $exchanges, 2 );
		$first = isset( $parts[0] ) ? $parts[0] : '';
		return sanitize_text_field( $first );
	}

	private function esc_classes( array $classes ): string {
		$out = array_filter( array_map( 'sanitize_html_class', (array) $classes ) );
		return implode( ' ', $out );
	}
}
