<?php
/**
 * @package        solo
 * @copyright      2014 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license        GNU GPL version 3 or later
 */

namespace Solo\Model;

use Akeeba\Engine\Postproc\Connector\Amazon\Aws\Common\Credentials\Credentials;
use Akeeba\Engine\Postproc\Connector\Amazon\Aws\S3\S3Client;
use Akeeba\Engine\Postproc\Connector\Amazons3;
use Awf\Html\Select;
use Awf\Mvc\Model;
use Awf\Text\Text;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;

class S3import extends Model
{
	/**
	 * Maximum time to spend downloading files per request, in seconds
	 *
	 * @var   int
	 */
	protected $maxTimeAllowance = 10;

	/**
	 * Populate the S3 connection credentials
	 *
	 * @return  void
	 */
	public function getS3Credentials()
	{
		$config = Factory::getConfiguration();
		$defS3AccessKey = $config->get('engine.postproc.s3.accesskey', '');
		$defS3SecretKey = $config->get('engine.postproc.s3.privatekey', '');

		$this->savestate(true);

		// The following lines initialise the model state. Do not remove them.
		$accessKey = $this->getState('s3access', $defS3AccessKey, 'raw');
		$secretKey = $this->getState('s3secret', $defS3SecretKey, 'raw');

		$bucket = $this->getState('s3bucket', '', 'raw');
		$folder = $this->getState('folder', '', 'raw');
		$file = $this->getState('file', '', 'raw');
		$part = $this->getState('part', -1, 'int');
		$frag = $this->getState('frag', -1, 'int');
	}

	/**
	 * Set the S3 connection credentials
	 *
	 * @param   string $accessKey Access key
	 * @param   string $secretKey Private key
	 *
	 * @return  void
	 */
	public function setS3Credentials($accessKey, $secretKey)
	{
		$this->setState('s3access', $accessKey);
		$this->setState('s3secret', $secretKey);
	}

	/**
	 * Gets an S3 connector object
	 *
	 * @return  S3Client
	 */
	private function _getS3Object()
	{
		static $s3 = null;

		if (!is_object($s3))
		{
			// Prepare the credentials object
			$s3Access = $this->getState('s3access');
			$s3Secret = $this->getState('s3secret');

			$amazonCredentials = new Credentials(
				$s3Access,
				$s3Secret
			);

			// Prepare the client options array. See http://docs.aws.amazon.com/aws-sdk-php/guide/latest/configuration.html#client-configuration-options
			$clientOptions = array(
				'credentials' => $amazonCredentials,
				'scheme'      => 'https',
				// Right now we don't set up a signature method and region in the GUI
				/**
				 * 'signature'   => $signatureMethod,
				 * 'region'      => $region
				 * /**/
			);

			// If SSL is not enabled you must not provide the CA root file.
			if (defined('AKEEBA_CACERT_PEM') && ($clientOptions['scheme'] == 'https'))
			{
				$clientOptions['ssl.certificate_authority'] = AKEEBA_CACERT_PEM;
			}
			else
			{
				// If SSL is off or we don't have a cacert.pem file disable HTTPS
				$clientOptions['scheme'] = 'http';
				$clientOptions['ssl.certificate_authority'] = false;
			}

			// Create the S3 client instance
			$s3 = S3Client::factory($clientOptions);
		}

		return $s3;
	}

	/**
	 * Do I have enough information to connect to S3?
	 *
	 * @param   boolean $checkBucket Should I also check that a bucket name is set?
	 *
	 * @return  boolean
	 */
	private function _hasAdequateInformation($checkBucket = true)
	{
		$s3access = $this->getState('s3access');
		$s3secret = $this->getState('s3secret');
		$s3bucket = $this->getState('s3bucket');

		$check = !empty($s3access) && !empty($s3secret);

		if ($checkBucket)
		{
			$check = $check && !empty($s3bucket);
		}

		return $check;
	}

	/**
	 * Get a list of Amazon S3 buckets
	 *
	 * @return  array|false|null
	 */
	public function getBuckets()
	{
		$buckets = null;

		if (!is_array($buckets))
		{
			$buckets = array();

			if ($this->_hasAdequateInformation(false))
			{
				$s3 = $this->_getS3Object();
				$bucketsRaw = $s3->listBuckets();
			}

			if (!empty($bucketsRaw) && !empty($bucketsRaw['Buckets']))
			{
				foreach ($bucketsRaw['Buckets'] as $bucket)
				{
					$buckets[] = $bucket['Name'];
				}
			}
		}

		return $buckets;
	}

	/**
	 * Get a listing of the files and folders in an S3 bucket
	 *
	 * @return  array
	 */
	public function getContents()
	{
		$folders = null;
		$files = null;
		$root = $this->getState('folder', '/');

		if (!is_array($folders) || !is_array($files))
		{
			$folders = array();

			if ($this->_hasAdequateInformation())
			{
				// Get the S3 client
				$s3 = $this->_getS3Object();

				// Set up the object listing
				$listOptions = array(
					'Bucket'    => $this->getState('s3bucket'),
					'Delimiter' => '/',
					'Prefix'    => $root,
				);

				// Get the response from S3
				$rawResult = $s3->listObjects($listOptions);

				$mixedResults = array();

				if (isset($rawResult['Contents']) && count($rawResult['Contents']))
				{
					$mixedResults = $rawResult['Contents'];
				}

				if (isset($rawResult['CommonPrefixes']) && count($rawResult['CommonPrefixes']))
				{
					$mixedResults = array_merge($mixedResults, $rawResult['CommonPrefixes']);
				}

				if (count($mixedResults))
				{
					foreach ($mixedResults as $record)
					{
						$name = isset($record['Key']) ? $record['Key'] : $record['Prefix'];

						if (substr($name, -8) == '$folder$')
						{
							continue;
						}

						if (array_key_exists('Key', $record))
						{
							$extension = substr($name, -4);

							if (!in_array($extension, array('.zip', '.jpa', '.jps')))
							{
								continue;
							}

							$files[$name] = $record;
						}
						elseif (array_key_exists('Prefix', $record))
						{
							$folders[$name] = $record;
						}
					}
				}
			}
		}

		return array(
			'files'   => $files,
			'folders' => $folders,
		);
	}

	/**
	 * Get the HTML dropdown to select an S3 bucket
	 *
	 * @return  string
	 */
	public function getBucketsDropdown()
	{
		$options = array();
		$buckets = $this->getBuckets();
		$options[] = Select::option('', Text::_('S3IMPORT_LABEL_SELECTBUCKET'));

		if (!empty($buckets))
		{
			foreach ($buckets as $b)
			{
				$options[] = Select::option($b, $b);
			}
		}

		$selected = $this->getState('s3bucket', '');

		return $options[] = Select::genericList($options, 's3bucket', array('class' => 'form-control'), 'value', 'text', $selected);
	}

	/**
	 * Get the breadcrumbs you'll be using in the S3 import view
	 *
	 * @return  array
	 */
	public function getCrumbs()
	{
		$folder = $this->getState('folder', '', 'raw');

		if (!empty($folder))
		{
			$folder = rtrim($folder, '/');
			$crumbs = explode('/', $folder);
		}
		else
		{
			$crumbs = array();
		}

		return $crumbs;
	}

	/**
	 * Start or continue downloading a backup from S3 to our server
	 *
	 * @return  boolean|null
	 *
	 * @throws  \Exception
	 */
	public function downloadToServer()
	{
		if (!$this->_hasAdequateInformation())
		{
			throw new \Exception(Text::_('S3IMPORT_ERR_NOTENOUGHINFO'), 500);
		}

		// Gather the necessary information to perform the download
		$part = $this->getState('part', -1, 'int');
		$frag = $this->getState('frag', -1, 'int');
		$remoteFilename = $this->getState('file', '', 'raw');

		$s3 = $this->_getS3Object();

		// Get the number of parts and total size from the session, or –if not there– fetch it
		$totalParts = $this->getState('totalparts', -1, 'int');
		$totalsize = $this->getState('totalsize', -1, 'int');

		if (($totalParts < 0) || (($part < 0) && ($frag < 0)))
		{
			$filePrefix = substr($remoteFilename, 0, -3);
			$listOptions = array(
				'Bucket'    => $this->getState('s3bucket'),
				'Delimiter' => '/',
				'Prefix'    => $filePrefix,
			);
			$rawResult = $s3->listObjects($listOptions);
			$totalsize = 0;

			if (count($rawResult['Contents']))
			{
				foreach ($rawResult['Contents'] as $file)
				{
					$totalsize += $file['Size'];
				}
			}

			$this->setState('totalparts', count($rawResult['Contents']));
			$this->setState('totalsize', $totalsize);
			$this->setState('donesize', 0);

			$totalParts = $this->getState('totalparts', -1, 'int');
		}

		// Start timing ourselves
		$timer = Factory::getTimer(); // The core timer object
		$start = $timer->getRunningTime(); // Mark the start of this download
		$break = false; // Don't break the step

		while (($timer->getRunningTime() < $this->maxTimeAllowance) && !$break && ($part < $totalParts))
		{
			// Get the remote and local filenames
			$basename = basename($remoteFilename);
			$extension = strtolower(str_replace(".", "", strrchr($basename, ".")));

			if ($part > 0)
			{
				$new_extension = substr($extension, 0, 1) . sprintf('%02u', $part);
			}
			else
			{
				$new_extension = $extension;
			}

			$remote_filename = substr($remoteFilename, 0, -strlen($extension)) . $new_extension;

			// Figure out where on Earth to put that file
			$local_file = Factory::getConfiguration()
					->get('akeeba.basic.output_directory') . '/' . basename($remote_filename);

			// Do we have to initialize the process?
			if ($part == -1)
			{
				// Currently downloaded size
				$this->setState('donesize', 0);
				// Init
				$part = 0;
			}

			// Do we have to initialize the file?
			if ($frag == -1)
			{
				// Delete and touch the output file
				Platform::getInstance()->unlink($local_file);
				$fp = @fopen($local_file, 'wb');

				if ($fp !== false)
				{
					@fclose($fp);
				}

				// Init
				$frag = 0;
			}

			// Calculate from and length
			$length = 1048576;

			$from = (int)($frag * $length);
			$to = (int)(($frag + 1) * $length - 1);

			// Try to download the first frag
			$temp_file = $local_file . '.tmp';
			@unlink($temp_file);
			$required_time = 1.0;
			$arguments = array(
				'Bucket' => $this->getState('s3bucket', ''),
				'Key'    => $remote_filename,
				'SaveAs' => $temp_file,
				'Range'  => "bytes=$from-$to",
			);

			try
			{
				$result = $s3->getObject($arguments);

				// Add the currently downloaded frag to the total size of downloaded files
				clearstatcache();
				$filesize = (int)@filesize($temp_file);
				$total = $this->getState('donesize', 0, 'int');
				$total += $filesize;
				$this->setState('donesize', $total);

				// Successful download, or have to move to the next part.
				// Append the file
				$fp = @fopen($local_file, 'ab');
				if ($fp === false)
				{
					// Can't open the file for writing
					@unlink($temp_file);

					throw new \Exception(Text::_('S3IMPORT_ERR_CANTWRITE'), 500);
				}

				$tf = fopen($temp_file, 'rb');

				while (!feof($tf))
				{
					$data = fread($tf, 262144);
					fwrite($fp, $data);
				}

				fclose($tf);
				fclose($fp);
				@unlink($temp_file);

				$frag++;
			}
			catch (\Exception $e)
			{
				@unlink($temp_file);

				if (
				(
					(($part < $totalParts) || (($totalParts == 1) && ($part == 0))) &&
					($frag == 0)
				)
				)
				{
					// Failure to download the part's beginning = failure to download. Period.
					throw new \Exception(Text::_('S3IMPORT_ERR_NOTFOUND'), 500);
				}
				elseif ($part >= $totalParts)
				{
					// Just finished! Create a stats record.
					$multipart = $totalParts;
					$multipart--;

					$filetime = time();

					// Create a new backup record
					$record = array(
						'description'     => Text::_('DISCOVER_LABEL_IMPORTEDDESCRIPTION'),
						'comment'         => '',
						'backupstart'     => date('Y-m-d H:i:s', $filetime),
						'backupend'       => date('Y-m-d H:i:s', $filetime + 1),
						'status'          => 'complete',
						'origin'          => 'backend',
						'type'            => 'full',
						'profile_id'      => 1,
						'archivename'     => basename($remoteFilename),
						'absolute_path'   => dirname($local_file) . '/' . basename($remoteFilename),
						'multipart'       => $multipart,
						'tag'             => 'backend',
						'filesexist'      => 1,
						'remote_filename' => '',
						'total_size'      => $totalsize
					);

					$id = null;
					$dummy = null;
					Platform::getInstance()->set_or_update_statistics($id, $record, $dummy);

					return null;
				}
				else
				{
					// Since this is a staggered download, consider this normal and go to the next part.
					$part++;
					$frag = -1;
				}
			}

			// Advance the frag pointer and mark the end
			$end = $timer->getRunningTime();

			// Do we predict that we have enough time?
			$required_time = max(1.1 * ($end - $start), $required_time);

			if ($required_time > ($this->maxTimeAllowance - $end + $start))
			{
				$break = true;
			}

			$start = $end;
		}

		// Pass the id, part, frag in the request so that the view can grab it
		$this->setState('part', $part);
		$this->setState('frag', $frag);

		if ($part >= $totalParts)
		{
			$local_file = Factory::getConfiguration()
					->get('akeeba.basic.output_directory') . '/' . basename($remoteFilename);
			// Just finished! Create a new backup record
			$record = array(
				'description'     => Text::_('DISCOVER_LABEL_IMPORTEDDESCRIPTION'),
				'comment'         => '',
				'backupstart'     => date('Y-m-d H:i:s'),
				'backupend'       => date('Y-m-d H:i:s', time() + 1),
				'status'          => 'complete',
				'origin'          => 'backend',
				'type'            => 'full',
				'profile_id'      => 1,
				'archivename'     => basename($remoteFilename),
				'absolute_path'   => dirname($local_file) . '/' . basename($remoteFilename),
				'multipart'       => $totalParts,
				'tag'             => 'backend',
				'filesexist'      => 1,
				'remote_filename' => '',
				'total_size'      => $totalsize
			);

			$id = null;
			$dummy = null;
			$id = Platform::getInstance()->set_or_update_statistics($id, $record, $dummy);

			return null;
		}

		return true;
	}

	public function setState($property, $value = null)
	{
		$session = $this->container->segment;
		$hash = $this->getHash();

		$session->{$hash . $property} = $value;

		return parent::setState($property, $value);
	}
}