<?php
if ( ! defined( 'ABSPATH' ) ) {
	die( 'Direct access is not allowed.' );
}

if ( ! class_exists( 'WP_List_Table' ) ) {
	require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}

/**
 * List table for ecommerce logs on the FinalPOS Dashboard logs tab.
 */
class FinalPOS_Logs_List_Table extends WP_List_Table {

	/**
	 * Filters for the log query (source, type, entity_type, entity_id, sort_by).
	 *
	 * @var array
	 */
	protected $filters = array();

	/**
	 * Company ID for the API request.
	 *
	 * @var string
	 */
	protected $company_id = '';

	/**
	 * Error message when fetch fails (e.g. 401 or connection error).
	 *
	 * @var string
	 */
	protected $logs_error = '';

	/**
	 * Map of column names to API attribute keys.
	 *
	 * @var array<string, string>
	 */
	const COLUMN_ATTRIBUTE_MAP = array(
		'created'     => 'created_at',
		'type'        => 'type',
		'entity_type' => 'entity_type',
		'message'     => 'message',
		'entity_id'   => 'entity_id',
	);

	/**
	 * Constructor.
	 *
	 * @param array  $filters    Associative array: source, type, entity_type, entity_id, sort_by.
	 * @param string $company_id Company ID for hub API.
	 */
	public function __construct( $filters = array(), $company_id = '' ) {
		$this->filters    = is_array( $filters ) ? $filters : array();
		$this->company_id = is_string( $company_id ) ? $company_id : '';

		parent::__construct(
			array(
				'plural'   => 'finalpos-logs',
				'singular' => 'finalpos-log',
				'ajax'     => false,
				'screen'   => 'toplevel_page_' . FINALPOS_PAGE_DASHBOARD,
			)
		);
	}

	/**
	 * Get the logs error message set during prepare_items() (e.g. 401 or connection failure).
	 *
	 * @return string
	 */
	public function get_logs_error() {
		return $this->logs_error;
	}

	/**
	 * Fetch data and set items and pagination.
	 *
	 * Expects hub-api ecommerce-log list response (JSON:API 1.1). Example response body:
	 *
	 * {
	 *   "data": [
	 *     {
	 *       "type": "ecommerceLogs",
	 *       "id": "a1b2c3d4e5f6789012345678",
	 *       "attributes": {
	 *         "company_id": "company-123",
	 *         "source": "woo-commerce",
	 *         "type": "order.upsert_completed",
	 *         "message": "Order 1001 synced successfully",
	 *         "recommendation_message": null,
	 *         "entity_type": "order",
	 *         "entity_id": "1001",
	 *         "sync_job_id": "job-abc",
	 *         "scope": "full",
	 *         "metadata": "{}",
	 *         "created_at": "2025-03-06 12:00:00"
	 *       }
	 *     }
	 *   ],
	 *   "meta": {
	 *     "total": 42,
	 *     "page": 1,
	 *     "limit": 20,
	 *     "totalPages": 3
	 *   },
	 *   "links": {
	 *     "self": "https://api.example.com/ecommerce-log?page=1&limit=20",
	 *     "first": "https://api.example.com/ecommerce-log?page=1&limit=20",
	 *     "last": "https://api.example.com/ecommerce-log?page=3&limit=20",
	 *     "next": "https://api.example.com/ecommerce-log?page=2&limit=20"
	 *   },
	 *   "jsonapi": { "version": "1.1" }
	 * }
	 */
	public function prepare_items() {
		$this->_column_headers = array(
			$this->get_columns(),
			array(),
			$this->get_sortable_columns(),
			$this->get_primary_column_name(),
		);
		$this->logs_error      = '';
		$page                  = max( 1, $this->get_pagenum() );
		$response              = FinalPOS_Dashboard::fetch_ecommerce_logs( $page, $this->filters, $this->company_id );

		if ( empty( $response['success'] ) ) {
			$status_code = isset( $response['status_code'] ) ? (int) $response['status_code'] : 0;
			if ( $status_code === 401 || $status_code === 403 ) {
				$this->logs_error = __( 'Cannot connect to FinalPOS: authentication failed. Please reconnect the plugin.', 'finalpos' );
			} elseif ( $status_code >= 500 ) {
				$this->logs_error = sprintf(
					/* translators: %d: HTTP status code */
					__( 'Cannot connect to FinalPOS: server error (HTTP %d). Please try again later.', 'finalpos' ),
					$status_code
				);
			} elseif ( $status_code === 0 ) {
				$this->logs_error = __( 'Cannot connect to FinalPOS. Please check your internet connection and try again.', 'finalpos' );
			} else {
				$this->logs_error = sprintf(
					/* translators: %d: HTTP status code */
					__( 'Cannot connect to FinalPOS (HTTP %d). Please try again.', 'finalpos' ),
					$status_code
				);
			}
			$this->items = array();
			$this->set_pagination_args(
				array(
					'total_items' => 0,
					'total_pages' => 0,
					'per_page'    => FINALPOS_ECOMMERCE_LOG_PER_PAGE,
				)
			);
			return;
		}

		$document = $this->normalize_jsonapi_response_body( $response );
		if ( $document === null ) {
			$this->logs_error = __( 'Cannot connect to FinalPOS: received an unexpected response. Please try again.', 'finalpos' );
			$this->items      = array();
			$this->set_pagination_args(
				array(
					'total_items' => 0,
					'total_pages' => 0,
					'per_page'    => FINALPOS_ECOMMERCE_LOG_PER_PAGE,
				)
			);
			return;
		}

		// data: array of { type, id, attributes } (JSON:API resource objects).
		$data  = isset( $document['data'] ) ? $document['data'] : array();
		$data  = $this->ensure_data_is_resource_list( $data );
		$items = array();
		foreach ( $data as $resource ) {
			$item = $this->jsonapi_resource_to_item( $resource );
			if ( $item !== null ) {
				$items[] = $item;
			}
		}
		$this->items = $items;

		// meta: { total, page, limit, totalPages }.
		$meta        = isset( $document['meta'] ) && is_array( $document['meta'] ) ? $document['meta'] : array();
		$total       = isset( $meta['total'] ) ? (int) $meta['total'] : count( $this->items );
		$total_pages = isset( $meta['totalPages'] ) ? max( 1, (int) $meta['totalPages'] ) : 1;
		$per_page    = isset( $meta['limit'] ) ? max( 1, (int) $meta['limit'] ) : FINALPOS_ECOMMERCE_LOG_PER_PAGE;

		$this->set_pagination_args(
			array(
				'total_items' => $total,
				'total_pages' => $total_pages,
				'per_page'    => $per_page,
			)
		);
	}

	/**
	 * Normalize hub-api response to a JSON:API document array or null.
	 *
	 * @param array $response Response from FinalPOS_Dashboard::fetch_ecommerce_logs (success, body, status_code).
	 * @return array|null Document with keys data, meta, links, jsonapi; or null on failure / invalid body.
	 */
	protected function normalize_jsonapi_response_body( $response ) {
		if ( empty( $response['success'] ) ) {
			return null;
		}
		$body = isset( $response['body'] ) ? $response['body'] : null;
		if ( is_string( $body ) ) {
			$decoded = json_decode( $body, true );
			$body    = is_array( $decoded ) ? $decoded : null;
		}
		if ( $body !== null && is_object( $body ) ) {
			$body = (array) $body;
		}
		return is_array( $body ) ? $body : null;
	}

	/**
	 * Ensure data is a list of resource objects (API may return a single resource as object).
	 *
	 * @param mixed $data Value of document['data'] (array or single resource object).
	 * @return array List of resource arrays (each with type, id, attributes).
	 */
	protected function ensure_data_is_resource_list( $data ) {
		if ( ! is_array( $data ) ) {
			return array();
		}
		// Single resource object: has 'attributes' but no numeric keys.
		if ( ! empty( $data ) && isset( $data['attributes'] ) && ! isset( $data[0] ) ) {
			return array( $data );
		}
		return $data;
	}

	/**
	 * Map one JSON:API resource object to a table row item.
	 *
	 * @param mixed $resource One element from document['data'] (type, id, attributes).
	 * @return array|null Item array with id, type, attributes; or null if invalid.
	 */
	protected function jsonapi_resource_to_item( $resource ) {
		$row = is_object( $resource ) ? (array) $resource : $resource;
		if ( ! is_array( $row ) ) {
			return null;
		}
		$attributes = isset( $row['attributes'] ) && is_array( $row['attributes'] )
			? $row['attributes']
			: ( isset( $row['attributes'] ) && is_object( $row['attributes'] ) ? (array) $row['attributes'] : array() );
		// Convert created_at from GMT to site/local time (WordPress timezone).
		// TODO: Need to discuss implementations
		// if ( ! empty( $attributes['created_at'] ) && is_string( $attributes['created_at'] ) ) {
		// $attributes['created_at'] = $this->gmt_to_local_date( $attributes['created_at'] );
		// }
		return array(
			'id'         => isset( $row['id'] ) ? (string) $row['id'] : '',
			'type'       => isset( $row['type'] ) ? (string) $row['type'] : '',
			'attributes' => $attributes,
		);
	}

	/**
	 * Convert a GMT datetime string to the site's local time using WordPress timezone.
	 *
	 * @param string $gmt_date Datetime string in GMT (e.g. "Y-m-d H:i:s" from API).
	 * @return string Same format in site/local time, or original string if conversion fails.
	 */
	protected function gmt_to_local_date( $gmt_date ) {
		$timestamp = strtotime( $gmt_date . ' UTC' );
		if ( $timestamp === false ) {
			return $gmt_date;
		}
		if ( function_exists( 'wp_date' ) ) {
			return wp_date( 'Y-m-d H:i:s', $timestamp );
		}
		return date( 'Y-m-d H:i:s', $timestamp );
	}

	/**
	 * Column headers.
	 *
	 * @return array
	 */
	public function get_columns() {
		return array(
			'created'     => __( 'Created', 'finalpos' ),
			'type'        => __( 'Type', 'finalpos' ),
			'entity_type' => __( 'Entity type', 'finalpos' ),
			'message'     => __( 'Message', 'finalpos' ),
			'entity_id'   => __( 'Entity ID', 'finalpos' ),
		);
	}

	/**
	 * Type column: show type with WordPress notice-style class for text color.
	 *
	 * @param array $item Row data (resource with 'attributes').
	 * @return string
	 */
	protected function column_type( $item ) {
		if ( is_object( $item ) ) {
			$item = (array) $item;
		}
		$attrs = isset( $item['attributes'] ) && is_array( $item['attributes'] )
			? $item['attributes']
			: ( isset( $item['attributes'] ) && is_object( $item['attributes'] ) ? (array) $item['attributes'] : array() );
		$type  = isset( $attrs['type'] ) ? (string) $attrs['type'] : '';
		$label = $type !== '' ? $type : '—';

		// Map log type to WordPress notice class for consistent admin styling.
		$notice_class = 'notice';
		if ( in_array( $type, array( 'error' ), true ) ) {
			$notice_class = 'notice notice-error';
		} elseif ( in_array( $type, array( 'warning' ), true ) ) {
			$notice_class = 'notice notice-warning';
		} elseif ( in_array( $type, array( 'notification' ), true ) ) {
			$notice_class = 'notice notice-info';
		} elseif ( in_array( $type, array( 'debug' ), true ) ) {
			$notice_class = 'notice notice-debug';
		}

		return '<span class="finalpos-log-type ' . esc_attr( $notice_class ) . '">' . esc_html( $label ) . '</span>';
	}

	/**
	 * Entity ID column: show as link to WordPress/WooCommerce entity when it exists.
	 *
	 * @param array $item Row data (resource with 'attributes').
	 * @return string
	 */
	protected function column_entity_id( $item ) {
		if ( is_object( $item ) ) {
			$item = (array) $item;
		}
		$attrs = isset( $item['attributes'] ) && is_array( $item['attributes'] )
			? $item['attributes']
			: ( isset( $item['attributes'] ) && is_object( $item['attributes'] ) ? (array) $item['attributes'] : array() );
		$entity_type = isset( $attrs['entity_type'] ) ? (string) $attrs['entity_type'] : '';
		$entity_id   = array_key_exists( 'entity_id', $attrs ) ? $attrs['entity_id'] : '';
		if ( $entity_id === null || $entity_id === '' ) {
			return '';
		}
		$entity_id = (string) $entity_id;
		$url       = $this->get_entity_edit_url( $entity_type, $entity_id );
		if ( $url !== null && $url !== '' ) {
			$label = $this->get_entity_type_label( $entity_type );
			$text  = $label !== '' ? $label . ': ' . $entity_id : $entity_id;
			return '<a href="' . esc_url( $url ) . '" class="finalpos-entity-id-link">' . esc_html( $text ) . '</a>';
		}
		return esc_html( $entity_id );
	}

	/**
	 * Translated label for entity type (e.g. Order, Product). Empty string if type unknown.
	 *
	 * @param string $entity_type Entity type key (FINALPOS_ENTITY_TYPE_* constant).
	 * @return string
	 */
	protected function get_entity_type_label( $entity_type ) {
		if ( ! array_key_exists( $entity_type, FINALPOS_ECOMMERCE_LOG_ENTITY_TYPES ) ) {
			return '';
		}
		return __( FINALPOS_ECOMMERCE_LOG_ENTITY_TYPES[ $entity_type ], 'finalpos' );
	}

	/**
	 * Get admin edit URL for a given entity type and ID if the entity exists in WordPress/WooCommerce.
	 *
	 * Uses WordPress and WooCommerce APIs. Returns null if entity type is unknown or entity does not exist.
	 *
	 * @param string $entity_type Entity type (FINALPOS_ENTITY_TYPE_* constant).
	 * @param string $entity_id   Entity ID (numeric string or ID).
	 * @return string|null Edit URL or null when entity not found or type not mappable.
	 */
	protected function get_entity_edit_url( $entity_type, $entity_id ) {
		$id = is_numeric( $entity_id ) ? absint( $entity_id ) : 0;
		if ( $id === 0 && $entity_id !== '0' ) {
			return null;
		}

		switch ( $entity_type ) {
			case FINALPOS_ENTITY_TYPE_ORDER:
				if ( function_exists( 'wc_get_order' ) ) {
					$order = wc_get_order( $id );
					if ( $order && is_a( $order, 'WC_Order' ) ) {
						return $order->get_edit_order_url();
					}
				}
				return null;

			case FINALPOS_ENTITY_TYPE_REFUND:
				if ( function_exists( 'wc_get_order' ) ) {
					$refund = wc_get_order( $id );
					if ( $refund && is_a( $refund, 'WC_Order_Refund' ) ) {
						$parent_id = $refund->get_parent_id();
						if ( $parent_id ) {
							$parent = wc_get_order( $parent_id );
							if ( $parent && is_a( $parent, 'WC_Order' ) ) {
								return $parent->get_edit_order_url();
							}
						}
					}
				}
				return null;

			case FINALPOS_ENTITY_TYPE_PRODUCT:
				$link = get_edit_post_link( $id, 'raw' );
				return $link ? $link : null;

			case FINALPOS_ENTITY_TYPE_VARIANT:
				$parent_id = get_post_field( 'post_parent', $id );
				if ( $parent_id ) {
					$link = get_edit_post_link( $parent_id, 'raw' );
				} else {
					$link = get_edit_post_link( $id, 'raw' );
				}
				return $link ? $link : null;

			case FINALPOS_ENTITY_TYPE_CUSTOMER:
				if ( get_userdata( $id ) ) {
					$link = get_edit_user_link( $id );
					return $link ? $link : null;
				}
				return null;

			default:
				return null;
		}
	}

	/**
	 * Default column output.
	 *
	 * @param array  $item        Row data (resource with 'attributes').
	 * @param string $column_name Column key.
	 * @return string
	 */
	protected function column_default( $item, $column_name ) {
		if ( is_object( $item ) ) {
			$item = (array) $item;
		}
		$attrs = isset( $item['attributes'] ) && is_array( $item['attributes'] )
			? $item['attributes']
			: ( isset( $item['attributes'] ) && is_object( $item['attributes'] ) ? (array) $item['attributes'] : array() );
		$key   = isset( self::COLUMN_ATTRIBUTE_MAP[ $column_name ] ) ? self::COLUMN_ATTRIBUTE_MAP[ $column_name ] : $column_name;
		$value = array_key_exists( $key, $attrs ) ? $attrs[ $key ] : ( array_key_exists( $key, $item ) ? $item[ $key ] : '' );
		if ( $value === null ) {
			$value = '';
		}
		if ( $column_name === 'created' && ( is_array( $value ) || is_object( $value ) ) ) {
			$value = $this->format_created_at_value( $value );
		}
		return esc_html( (string) $value );
	}

	/**
	 * Format createdAt when it comes as array/object (e.g. JSON date).
	 *
	 * @param mixed $value Raw value (array or object with $date / value / timestamp).
	 * @return string Readable date or original cast to string.
	 */
	protected function format_created_at_value( $value ) {
		if ( is_array( $value ) ) {
			if ( isset( $value['$date'] ) ) {
				return $value['$date'];
			}
			if ( isset( $value['value'] ) ) {
				return $value['value'];
			}
			if ( isset( $value['date'] ) ) {
				return $value['date'];
			}
		}
		if ( is_object( $value ) ) {
			$arr = (array) $value;
			if ( isset( $arr['$date'] ) ) {
				return $arr['$date'];
			}
			if ( isset( $arr['value'] ) ) {
				return $arr['value'];
			}
		}
		return '';
	}

	/**
	 * No bulk actions.
	 *
	 * @return array
	 */
	protected function get_bulk_actions() {
		return array();
	}

	/**
	 * Message when there are no items.
	 */
	public function no_items() {
		esc_html_e( 'No log entries found.', 'finalpos' );
	}

	/**
	 * Add finalpos-logs-table to table classes for existing CSS.
	 *
	 * @return string[]
	 */
	protected function get_table_classes() {
		return array_merge( parent::get_table_classes(), array( 'finalpos-logs-table' ) );
	}

	/**
	 * Show table nav (pagination) only at the bottom to avoid duplicate pagination.
	 *
	 * @param string $which The location of the navigation: 'top' or 'bottom'.
	 */
	protected function display_tablenav( $which ) {
		parent::display_tablenav( $which );
	}
}
