<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Sections and pages that travel from one site to another.
 *
 * WHAT IT SOLVES. A good section is born on one site and stays there. Whoever builds sites for a
 * living wants that same well-made hero on the next client, and today the only road is to
 * regenerate it — paying again, and getting something similar but not the same.
 *
 * WHAT TRAVELS AND WHAT DOES NOT. The TEMPLATE travels, that is, the structure: markup, CSS,
 * placeholders, directives. The origin site's content does not — loops draw from WordPress, and on
 * the other site they will draw from another WordPress, which is exactly what is wanted. Field
 * values travel because without them the section would arrive mute, but they are texts to be
 * rewritten, not anybody's data.
 *
 * THE TWO THINGS THAT CANNOT BE CARRIED, and that have to be SAID rather than discovered:
 *
 *  - the design system's VARIABLES: a section using var(--aipb-color-brand) picks up the other
 *    site's brand. That is almost always what is wanted, but if that colour does not exist there,
 *    the variable is empty and the CSS is worth nothing;
 *  - REFERENCES to things the destination might not have: a type="prodotto" where products do not
 *    exist, a shortcode from a plugin that is not installed, a menu whose id is another menu there.
 *
 * Which is why importing is not silent: it says what it found and what is missing, and lets
 * somebody decide.
 */
class AIPB_Portable {

	/** The file format. It goes up when the SHAPE changes, not on every plugin release. */
	const FORMAT = 1;

	/** The kinds of requirement a package can carry, and that an import checks. */
	const REQUIREMENTS = array( 'types', 'taxonomies', 'fields', 'shortcodes', 'vars' );

	/**
	 * Packages one or more sections.
	 *
	 * @param array  $blocks [ { name, template, schema, values } ]
	 * @param string $title  A readable name for the package.
	 *
	 * @return array
	 */
	public static function export( $blocks, $title = '' ) {
		$sections = array();

		foreach ( (array) $blocks as $block ) {
			$template = isset( $block['template'] ) ? (string) $block['template'] : '';
			if ( '' === $template ) {
				continue; // a block with no markup of its own is not a section: it is not exported
			}

			$sections[] = array(
				'name'     => isset( $block['name'] ) ? sanitize_text_field( (string) $block['name'] ) : 'Section',
				'template' => $template,
				'schema'   => isset( $block['schema'] ) && is_array( $block['schema'] ) ? $block['schema'] : array( 'fields' => array() ),
				'values'   => isset( $block['values'] ) && is_array( $block['values'] ) ? $block['values'] : array(),
			);
		}

		return array(
			'format'   => self::FORMAT,
			'aura'     => AIPB_VERSION,
			'dsl'      => AIPB_Template_Engine::DSL,
			'title'    => sanitize_text_field( $title ),
			'origin'   => home_url(),
			'exported' => gmdate( 'c' ),
			// What the destination site needs for these sections to really work. Not a constraint:
			// it is what the import compares, so it can say it up front instead of leaving it to be
			// discovered from a page that came out wrong.
			'requires' => self::requirements( $sections ),
			'sections' => $sections,
		);
	}

	/**
	 * What a package demands: content types, taxonomies, fields, shortcodes, variables.
	 *
	 * Read from the templates, not from a hand-written list: a hand-written list is the one nobody
	 * remembers to update, and then it lies exactly when it matters.
	 */
	public static function requirements( $sections ) {
		$all = '';
		foreach ( (array) $sections as $section ) {
			$all .= "\n" . ( isset( $section['template'] ) ? (string) $section['template'] : '' );
		}

		$out = array_fill_keys( self::REQUIREMENTS, array() );

		$patterns = array(
			'types'      => '/\btype="([a-z0-9_-]+)"/i',
			'taxonomies' => '/\btaxonomy="([a-z0-9_-]+)"/i',
			'fields'     => '/\{\{[a-z]:(?:post|term|field)\.(?:acf|meta)_([a-zA-Z0-9_]+)\}\}/',
			'shortcodes' => '/\{\{s:([a-zA-Z0-9_-]+)/',
			'vars'       => '/var\(\s*(--aipb-[a-z0-9-]+)/i',
		);

		foreach ( $patterns as $kind => $pattern ) {
			if ( preg_match_all( $pattern, $all, $m ) ) {
				$out[ $kind ] = array_values( array_unique( $m[1] ) );
			}
		}

		return $out;
	}

	/**
	 * What is missing on THIS site for the package to work.
	 *
	 * @return array{missing:array,ready:bool}
	 */
	public static function inspect( $package ) {
		$required = isset( $package['requires'] ) && is_array( $package['requires'] )
			? $package['requires']
			: self::requirements( isset( $package['sections'] ) ? $package['sections'] : array() );

		$missing = array_fill_keys( self::REQUIREMENTS, array() );

		foreach ( self::required( $required, 'types' ) as $type ) {
			// "prop" is a section's internal repeater, not a content type.
			if ( 'prop' !== $type && ! post_type_exists( $type ) ) {
				$missing['types'][] = $type;
			}
		}

		foreach ( self::required( $required, 'taxonomies' ) as $taxonomy ) {
			if ( ! taxonomy_exists( $taxonomy ) ) {
				$missing['taxonomies'][] = $taxonomy;
			}
		}

		// A field exists if ANY of the site's content types declares it: there is no knowing which
		// type it belonged to on the other site, and asking would be false precision.
		$known = array();
		foreach ( (array) AIPB_Fields::all() as $fields ) {
			foreach ( $fields as $field ) {
				$known[ $field['key'] ] = true;
			}
		}
		foreach ( self::required( $required, 'fields' ) as $field ) {
			if ( ! isset( $known[ $field ] ) ) {
				$missing['fields'][] = $field;
			}
		}

		$registered = class_exists( 'AIPB_Shortcodes' ) ? array_flip( AIPB_Shortcodes::registered() ) : array();
		foreach ( self::required( $required, 'shortcodes' ) as $shortcode ) {
			if ( ! isset( $registered[ $shortcode ] ) ) {
				$missing['shortcodes'][] = $shortcode;
			}
		}

		$css = class_exists( 'AIPB_Design' ) ? AIPB_Design::css_variables() : '';
		foreach ( self::required( $required, 'vars' ) as $variable ) {
			if ( false === strpos( $css, $variable . ':' ) ) {
				$missing['vars'][] = $variable;
			}
		}

		$ready = true;
		foreach ( $missing as $list ) {
			if ( $list ) {
				$ready = false;
			}
		}

		return array( 'missing' => $missing, 'ready' => $ready );
	}

	/**
	 * Reads a package and returns the blocks ready to insert.
	 *
	 * It saves nothing: whoever imports has to be able to see what is about to come in, and the
	 * decision to write to the site stays with whoever pressed the button.
	 *
	 * @return array{sections:array,inspect:array}|WP_Error
	 */
	public static function import( $json ) {
		$data = is_array( $json ) ? $json : json_decode( (string) $json, true );

		if ( ! is_array( $data ) || empty( $data['sections'] ) || ! is_array( $data['sections'] ) ) {
			return new WP_Error(
				'aipb_import_format',
				__( 'This file does not contain any Aura sections.', 'aipb' ),
				array( 'status' => 400 )
			);
		}

		if ( (int) ( isset( $data['format'] ) ? $data['format'] : 0 ) > self::FORMAT ) {
			return new WP_Error(
				'aipb_import_version',
				__( 'This file was exported by a newer version of Aura: update the plugin before importing it.', 'aipb' ),
				array( 'status' => 400 )
			);
		}

		$sections = array();
		foreach ( $data['sections'] as $index => $section ) {
			if ( ! is_array( $section ) || empty( $section['template'] ) ) {
				continue;
			}

			$sections[] = array(
				'type' => 'inline',
				// The id is regenerated: the original belongs to another page, and two blocks with
				// the same id in one stack are a silent mess.
				'id'   => 'imp_' . wp_generate_password( 8, false, false ),
				'name' => isset( $section['name'] )
					? sanitize_text_field( (string) $section['name'] )
					/* translators: %d: the section's position in the imported file. */
					: sprintf( __( 'Section %d', 'aipb' ), $index + 1 ),
				'template' => (string) $section['template'],
				'schema'   => isset( $section['schema'] ) && is_array( $section['schema'] ) ? $section['schema'] : array( 'fields' => array() ),
				'values'   => isset( $section['values'] ) && is_array( $section['values'] ) ? $section['values'] : array(),
			);
		}

		if ( ! $sections ) {
			return new WP_Error(
				'aipb_import_empty',
				__( 'There is no readable section in this file.', 'aipb' ),
				array( 'status' => 400 )
			);
		}

		return array(
			'sections' => $sections,
			'inspect'  => self::inspect( $data ),
			'origin'   => isset( $data['origin'] ) ? esc_url_raw( (string) $data['origin'] ) : '',
			'title'    => isset( $data['title'] ) ? sanitize_text_field( (string) $data['title'] ) : '',
		);
	}

	/** One requirement list out of a package, always an array. */
	protected static function required( $requirements, $kind ) {
		return ( isset( $requirements[ $kind ] ) && is_array( $requirements[ $kind ] ) )
			? $requirements[ $kind ]
			: array();
	}
}
