<?php

namespace DeliciousBrains\WPMDB\Common\Error;

use DeliciousBrains\WPMDB\Common\Debug;
use DeliciousBrains\WPMDB\Common\Filesystem\Filesystem;
use DeliciousBrains\WPMDB\Common\MigrationPersistence\Persistence;
use DeliciousBrains\WPMDB\Common\MigrationState\MigrationStateManager;
use DeliciousBrains\WPMDB\Common\Properties\Properties;
use DeliciousBrains\WPMDB\Common\Settings\Settings;
use DeliciousBrains\WPMDB\Common\Util\Util;
use DeliciousBrains\WPMDB\WPMDBDI;
use Exception;

class ErrorLog {
	/**
	 * @var
	 */
	public $error_log;

	/**
	 * @var
	 */
	public $state_data;

	/**
	 * @var Properties
	 */
	public $props;

	/**
	 * @var Settings
	 */
	private $settings;

	/**
	 * @var Filesystem
	 */
	private $filesystem;

	/**
	 * @var $error
	 */
	private $error;

	/**
	 * @var Util
	 */
	private $util;

	public function __construct(
		Settings $settings,
		Filesystem $filesystem,
		Util $util,
		Properties $properties
	) {
		$this->props      = $properties;
		$this->settings   = $settings->get_settings();
		$this->filesystem = $filesystem;
		$this->util       = $util;

		add_action( 'wpmdb_track_migration_error', array( $this, 'automatically_log_migration_error' ) );
	}

	public function get_error_log() {
		return $this->error_log;
	}

	/**
	 * Loads the error log into the error log class property.
	 */
	function load_error_log() {
		$this->error_log = get_site_option( WPMDB_ERROR_LOG_OPTION );

		/*
		 * The error log was previously stored and retrieved using get_option and update_option respectively.
		 * Here we update the subsite option to a network wide option if applicable.
		 */
		if ( false === $this->error_log && is_multisite() && is_network_admin() ) {
			$this->error_log = get_option( WPMDB_ERROR_LOG_OPTION );
			if ( false !== $this->error_log ) {
				update_site_option( WPMDB_ERROR_LOG_OPTION, $this->error_log );
				delete_option( WPMDB_ERROR_LOG_OPTION );
			}
		}

		return $this->error_log;
	}

	/**
	 * Save an error, so it can be seen in the diagnostic info.
	 *
	 * @param string $wpmdb_error
	 * @param mixed  $additional_error_var
	 *
	 * @return void
	 */
	public function log_error( $wpmdb_error, $additional_error_var = false ) {
		try {
			$state_data = WPMDBDI::getInstance()->get( MigrationStateManager::class )->set_post_data();
		} catch ( Exception $e ) {
			Debug::log( 'Could not get the MigrationStateManager when trying to log an error. Error was: ' . $e->getMessage() );
			return;
		}

		// Oh dear, we really do have an issue!
		if ( is_wp_error( $state_data ) ) {
			return;
		}

		$error_header = "********************************************\n******  Log date: " . date( 'Y/m/d H:i:s' ) . " ******\n********************************************\n\n";
		$error        = $error_header;
		$error        .= 'Migration ID: ' . (string) Persistence::getMigrationId( $state_data ) . "\n";
		if ( isset( $state_data['intent'] ) ) {
			$error .= 'Intent: ' . $state_data['intent'] . "\n";
		}
		if ( isset( $state_data['stages'] ) ) {
			$error .= 'Stages: ' . implode( ' | ', json_decode( $state_data['stages'], true ) ) . "\n";
		}
		if ( isset( $state_data['action'] ) ) {
			$error .= 'Action: ' . $state_data['action'] . "\n";
		}
		if ( isset( $state_data['local'] ) && isset( $state_data['local']['site_url'] ) ) {
			$error .= 'Local: ' . $state_data['site_details']['local']['site_url'] . "\n";
		}
		if ( isset( $state_data['remote'] ) && isset( $state_data['remote']['site_url'] ) ) {
			$error .= 'Remote: ' . $state_data['site_details']['remote']['site_url'] . "\n";
		}
		if ( isset( $GLOBALS['wpmdb_meta'] ) && isset( $GLOBALS['wpmdb_meta']['wp-migrate-db-pro'] ) ) {
			$error .= 'WP Migrate Version: ' . $GLOBALS['wpmdb_meta']['wp-migrate-db-pro']['version'] . "\n\n";
		}
		$error .= 'Error: ' . $wpmdb_error . "\n\n";

		if ( ! empty( $this->props->attempting_to_connect_to ) ) {
			$ip    = $this->get_remote_ip( $this->props->attempting_to_connect_to );
			$error .= 'Attempted to connect to: ' . $this->props->attempting_to_connect_to . ' (' . ( $ip ? $ip : 'ip lookup failed' ) . ')' . "\n\n";
		}

		if ( $additional_error_var !== false ) {
			// don't print the whole response object to the log
			if ( is_array( $additional_error_var ) && isset( $additional_error_var['http_response'] ) ) {
				if ( isset( $additional_error_var['http_response'] ) && ( $additional_error_var['http_response'] instanceof \WP_HTTP_Requests_Response ) ) {
					$response                    = $additional_error_var['http_response']->get_response_object();
					$additional_error_var['url'] = $response->url;
				}
				unset( $additional_error_var['http_response'] );
			}

			$error .= print_r( $additional_error_var, true ) . "\n\n";
		}

		Debug::log( $error );

		$this->load_error_log();

		// Error log length in bytes (default 1Mb)
		$max_log_length            = apply_filters( 'wpmdb_max_error_log_length', 1000000 );
		$max_individual_log_length = apply_filters( 'wpmdb_max_individual_error_log_length', $max_log_length / 2.2 );

		// If error is longer than max individual log length, trim and add notice of doing so
		if ( strlen( $error ) > $max_individual_log_length ) {
			$length_trimmed = strlen( $error ) - $max_individual_log_length;
			$error          = substr( $error, 0, $max_individual_log_length );
			$error          .= "\n[$length_trimmed bytes were truncated from this error]\n\n";
		}

		// Trim existing log to accommodate new error if needed
		$existing_log_max_length = $max_log_length - strlen( $error );
		if ( strlen( $this->error_log ) > $existing_log_max_length ) {
			$this->error_log = substr( $this->error_log, -( $existing_log_max_length ) );

			// Crop at first log header
			$first_header_pos = strpos( $this->error_log, substr( $error_header, 0, strpos( $error_header, ' ' ) ) );
			if ( $first_header_pos ) {
				$this->error_log = substr( $this->error_log, $first_header_pos );
			}
		}

		if ( isset( $this->error_log ) ) {
			$this->error_log .= $error;
		} else {
			$this->error_log = $error;
		}

		update_site_option( WPMDB_ERROR_LOG_OPTION, $this->error_log );
	}

	function get_remote_ip( $url ) {
		$parsed_url = Util::parse_url( $url );
		if ( ! isset( $parsed_url['host'] ) ) {
			return false;
		}
		// '.' appended to host name to avoid issues with nslookup caching - see documentation of gethostbyname for more info
		$host = $parsed_url['host'] . '.';

		$ip = gethostbyname( $host );

		return ( $ip === $host ) ? false : $ip;
	}

	/**
	 * Check for wpmdb-download-log and related nonce
	 * if found begin diagnostic logging
	 *
	 * @return void
	 */
	function http_prepare_download_log() {
		if ( isset( $_GET['wpmdb-download-log'] ) && wp_verify_nonce( $_GET['nonce'], 'wpmdb-download-log' ) ) {
			ob_start();
			$this->output_diagnostic_info();
			$this->output_log_file();
			$log      = ob_get_clean();
			$url      = Util::parse_url( home_url() );
			$host     = sanitize_file_name( $url['host'] );
			$filename = sprintf( '%s-diagnostic-log-%s.txt', $host, date( 'YmdHis' ) );
			header( 'Content-Description: File Transfer' );
			header( 'Content-Type: application/octet-stream' );
			header( 'Content-Length: ' . strlen( $log ) );
			header( 'Content-Disposition: attachment; filename=' . $filename );
			echo $log;
			exit;
		}
	}

	/**
	 * Outputs useful diagnostic info text at the Diagnostic Info & Error Log
	 * section under the Help tab so the information can be viewed or
	 * downloaded and shared for debugging.
	 *
	 *
	 * @return void
	 */
	function output_diagnostic_info() {
		$diagnostic_info = $this->get_diagnostic_info();

		foreach ( $diagnostic_info as $section => $arr ) {
			$key_lengths    = array_map( 'strlen', array_keys( $arr ) );
			$max_key_length = max( $key_lengths );
			foreach ( $arr as $key => $val ) {
				if ( 0 === $key ) {
					echo $val . "\r\n";
					continue;
				}
				if ( is_array( $val ) ) {
					foreach ( $val as $subsection => $subval ) {
						echo " - ";
						if ( ! preg_match( '/^\d+$/', $subsection ) ) {
							echo "$subsection: ";
						}
						echo "$subval\r\n";
					}
					continue;
				}
				if ( ! preg_match( '/^\d+$/', $key ) ) {
					$pad_chr = '.';
					if ( $max_key_length - strlen( $key ) < 3 ) {
						$pad_chr = ' ';
					}
					echo str_pad( "$key: ", $max_key_length + 2, $pad_chr, STR_PAD_RIGHT );
				}
				echo " $val\r\n";
			}
			echo "\r\n";
		}

		return;
	}

	/**
	 * Gets diagnostic information about current site
	 *
	 * @return array
	 */
	function get_diagnostic_info() {
		global $wpdb;
		$diagnostic_info = array(); // group display sections into arrays

		$diagnostic_info['basic-info'] = array(
			'site_url()'  => site_url(),
			'home_url()'  => Util::home_url(),
			'site_path()' => esc_html( addslashes( Util::get_absolute_root_file_path() ) ),
		);

		$diagnostic_info['db-info'] = array(
			'Database Name' => $wpdb->dbname,
			'Table Prefix'  => $wpdb->base_prefix,
		);

		$diagnostic_info['wp-version'] = array(
			'WordPress Version' => get_bloginfo( 'version' ),
		);

		if ( is_multisite() ) {
			$diagnostic_info['multisite-info'] = array(
				'Multisite'            => defined( 'SUBDOMAIN_INSTALL' ) && SUBDOMAIN_INSTALL ? 'Sub-domain' : 'Sub-directory',
				'Domain Current Site'  => defined( 'DOMAIN_CURRENT_SITE' ) ? DOMAIN_CURRENT_SITE : 'Not Defined',
				'Path Current Site'    => defined( 'PATH_CURRENT_SITE' ) ? PATH_CURRENT_SITE : 'Not Defined',
				'Site ID Current Site' => defined( 'SITE_ID_CURRENT_SITE' ) ? SITE_ID_CURRENT_SITE : 'Not Defined',
				'Blog ID Current Site' => defined( 'BLOG_ID_CURRENT_SITE' ) ? BLOG_ID_CURRENT_SITE : 'Not Defined',
			);
		}

		$mdb_plugins  = array();
		$plugin_title = WPMDB_PLUGIN_TITLE;
		if ( $GLOBALS['wpmdb_meta'][ WPMDB_CORE_SLUG ]['version'] ) {
			$mdb_plugins[ $plugin_title ] = $GLOBALS['wpmdb_meta'][ WPMDB_CORE_SLUG ]['version'];
		}
		$diagnostic_info['mdb-plugins'] = $mdb_plugins;
		$curl_info                      = function_exists( 'curl_version' ) ? curl_version() : [];
		$diagnostic_info['server-info'] = array(
			'Web Server'                      => ! empty( $_SERVER['SERVER_SOFTWARE'] ) ? $_SERVER['SERVER_SOFTWARE'] : '',
			'PHP'                             => ( function_exists( 'phpversion' ) ) ? phpversion() : '',
			'WP Memory Limit'                 => WP_MEMORY_LIMIT,
			'PHP Memory Limit'                => ( function_exists( 'ini_get' ) ) ? ini_get( 'memory_limit' ) : '',
			'PHP Time Limit'                  => ( function_exists( 'ini_get' ) ) ? ini_get( 'max_execution_time' ) : '',
			'Set Time Limit Available'        => ( function_exists( 'set_time_limit' ) ) ? 'Yes' : 'No',
			'Blocked External HTTP Requests'  => ( defined( 'WP_HTTP_BLOCK_EXTERNAL' ) && WP_HTTP_BLOCK_EXTERNAL ) ? ( defined( 'WP_ACCESSIBLE_HOSTS' ) && ! empty( WP_ACCESSIBLE_HOSTS ) ? 'Partially (Accessible Hosts: ' . WP_ACCESSIBLE_HOSTS . ')' : 'All' ) : 'None',
			'fsockopen'                       => ( function_exists( 'fsockopen' ) ) ? 'Enabled' : 'Disabled',
			'OpenSSL'                         => ( $this->util->open_ssl_enabled() ) ? OPENSSL_VERSION_TEXT : 'Disabled',
			'cURL'                            => ( function_exists( 'curl_init' ) ) ? 'Enabled' : 'Disabled',
			'cURL Version'                    => isset( $curl_info['version'] ) ? $curl_info['version'] : 'Unknown',
			'Enable SSL verification setting' => ( 1 == $this->settings['verify_ssl'] ) ? 'Yes' : 'No',
			// phpcs:disable
			'Opcache Enabled'                 => ( function_exists( 'ini_get' ) && ini_get( 'opcache.enable' ) ) ? 'Enabled' : 'Disabled',
			// phpcs:enable
			'Basic Auth'                      => Util::basic_auth_enabled() ? 'Enabled' : 'Disabled',
		);

		$diagnostic_info['db-server-info'] = array(
			'MySQL'                    => Util::get_db_server_info(),
			'ext/mysqli'               => empty( $wpdb->use_mysqli ) ? 'no' : 'yes',
			'WP Locale'                => get_locale(),
			'DB Charset'               => defined( 'DB_CHARSET' ) ? DB_CHARSET : 'Not Defined',
			// Note that we use get_option here, not get_bloginfo, as get_bloginfo( 'charset' ) will return 'UTF-8' by default.
			'blog_charset'             => get_option( 'blog_charset', 'Unknown' ),
			'WPMDB_STRIP_INVALID_TEXT' => ( defined( 'WPMDB_STRIP_INVALID_TEXT' ) && WPMDB_STRIP_INVALID_TEXT ) ? 'Yes' : 'No',
		);

		$diagnostic_info['debug-settings'] = array(
			'Debug Mode'    => ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ? 'Yes' : 'No',
			'Debug Log'     => ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) ? 'Yes' : 'No',
			'Debug Display' => ( defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY ) ? 'Yes' : 'No',
			'Script Debug'  => ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? 'Yes' : 'No',
			'PHP Error Log' => ( function_exists( 'ini_get' ) ) ? ini_get( 'error_log' ) : '',
		);

		$server_limits = array(
			'WP Max Upload Size' => size_format( wp_max_upload_size() ),
			'PHP Post Max Size'  => size_format( $this->util->get_post_max_size() ),
		);

		if ( function_exists( 'ini_get' ) ) {
			if ( $suhosin_limit = ini_get( 'suhosin.post.max_value_length' ) ) {
				$server_limits['Suhosin Post Max Value Length'] = is_numeric( $suhosin_limit ) ? size_format( $suhosin_limit ) : $suhosin_limit;
			}
			if ( $suhosin_limit = ini_get( 'suhosin.request.max_value_length' ) ) {
				$server_limits['Suhosin Request Max Value Length'] = is_numeric( $suhosin_limit ) ? size_format( $suhosin_limit ) : $suhosin_limit;
			}
		}
		$diagnostic_info['server-limits'] = $server_limits;

		$diagnostic_info['mdb-settings'] = array(
			'WPMDB Bottleneck'       => size_format( $this->util->get_bottleneck() ),
			'Compatibility Mode'     => ( isset( $GLOBALS['wpmdb_compatibility']['active'] ) ) ? 'Yes' : 'No',
			'Delay Between Requests' => ( $this->settings['delay_between_requests'] > 0 ) ? $this->settings['delay_between_requests'] . 's' : 0,
		);

		$constants = array(
			'WP_HOME'        => ( defined( 'WP_HOME' ) && WP_HOME ) ? WP_HOME : 'Not defined',
			'WP_SITEURL'     => ( defined( 'WP_SITEURL' ) && WP_SITEURL ) ? WP_SITEURL : 'Not defined',
			'WP_CONTENT_URL' => ( defined( 'WP_CONTENT_URL' ) && WP_CONTENT_URL ) ? WP_CONTENT_URL : 'Not defined',
			'WP_CONTENT_DIR' => ( defined( 'WP_CONTENT_DIR' ) && WP_CONTENT_DIR ) ? WP_CONTENT_DIR : 'Not defined',
			'WP_PLUGIN_DIR'  => ( defined( 'WP_PLUGIN_DIR' ) && WP_PLUGIN_DIR ) ? WP_PLUGIN_DIR : 'Not defined',
			'WP_PLUGIN_URL'  => ( defined( 'WP_PLUGIN_URL' ) && WP_PLUGIN_URL ) ? WP_PLUGIN_URL : 'Not defined',
		);

		if ( is_multisite() ) {
			$constants['UPLOADS']        = ( defined( 'UPLOADS' ) && UPLOADS ) ? UPLOADS : 'Not defined';
			$constants['UPLOADBLOGSDIR'] = ( defined( 'UPLOADBLOGSDIR' ) && UPLOADBLOGSDIR ) ? UPLOADBLOGSDIR : 'Not defined';
		}

		$diagnostic_info['constants'] = $constants;

		$diagnostic_info = array_merge(
			$diagnostic_info,
			apply_filters( 'wpmdb_diagnostic_info', array(), $diagnostic_info )
		);

		$theme_info     = wp_get_theme();
		$theme_info_log = array(
			'Active Theme Name'   => $theme_info->Name,
			'Active Theme Folder' => $theme_info->get_stylesheet_directory(),
		);
		if ( $theme_info->get( 'Template' ) ) {
			$theme_info_log['Parent Theme Folder'] = $theme_info->get( 'Template' );
		}
		if ( ! $this->filesystem->file_exists( $theme_info->get_stylesheet_directory() ) ) {
			$theme_info_log['WARNING'] = 'Active Theme Folder Not Found';
		}
		$diagnostic_info['theme-info'] = $theme_info_log;

		$active_plugins_log = array( 'Active Plugins' );

		$active_plugins_log[1] = array();
		if ( isset( $GLOBALS['wpmdb_compatibility']['active'] ) ) {
			$whitelist = array_flip( (array) $this->settings['whitelist_plugins'] );
		} else {
			$whitelist = array();
		}
		$active_plugins = (array) get_option( 'active_plugins', array() );
		if ( is_multisite() ) {
			$network_active_plugins = wp_get_active_network_plugins();

			$active_plugins = array_map(
				array( $this->util, 'remove_wp_plugin_dir' ),
				$network_active_plugins
			);
		}
		foreach ( $active_plugins as $plugin ) {
			if ( ! file_exists( WP_PLUGIN_DIR . '/' . $plugin ) ) {
				continue;
			}

			$active_plugins_log[1][] = $this->util->get_plugin_details(
				WP_PLUGIN_DIR . '/' . $plugin,
				isset( $whitelist[ $plugin ] ) ? '*' : ''
			);
		}

		$diagnostic_info['active-plugins'] = $active_plugins_log;

		$mu_plugins = wp_get_mu_plugins();
		if ( $mu_plugins ) {
			$mu_plugins_log    = array( 'Must-Use Plugins' );
			$mu_plugins_log[1] = array();
			foreach ( $mu_plugins as $mu_plugin ) {
				$mu_plugins_log[1][] = $this->util->get_plugin_details( $mu_plugin );
			}
			$diagnostic_info['mu-plugins'] = $mu_plugins_log;
		}

		return $diagnostic_info;
	}

	function output_log_file() {
		$error_log = $this->load_error_log();
		if ( ! empty( $error_log ) ) {
			echo $error_log;
		}
	}

	/**
	 * @return mixed
	 */
	public function getError() {
		return $this->error;
	}

	/**
	 * @param mixed $error
	 */
	public function setError( $error ) {
		$this->error = $error;
	}

	/**
	 * Automatically log migration error
	 *
	 * @param array $error
	 *
	 * @return void
	 * @throws Exception
	 * @handles wpmdb_track_migration_error
	 */
	public function automatically_log_migration_error( $error ) {
		if ( ! empty( $error['message'] ) ) {
			$this->log_error( $error['message'] );
		}
	}
}
