<?php

namespace XF\Service\Style;

use League\Flysystem\FilesystemException;
use XF\App;
use XF\Entity\AddOn;
use XF\Entity\Style;
use XF\PrintableException;
use XF\Service\AbstractService;
use XF\Util\File;

use function strlen;

class ArchiveExportService extends AbstractService
{
	public const UPLOAD_DIR = 'upload';
	public const XML_FILE_NAME = 'style.xml';

	/**
	 * @var Style
	 */
	protected $style;

	/**
	 * @var AddOn|null
	 */
	protected $addOn;

	/**
	 * @var ExportService
	 */
	protected $xmlExporter;

	/**
	 * @var bool
	 */
	protected $independent = false;

	public function __construct(App $app, Style $style)
	{
		parent::__construct($app);

		$this->style = $style;
		$this->xmlExporter = $this->service(ExportService::class, $style);
	}

	public function getStyle(): Style
	{
		return $this->style;
	}

	public function setAddOn(?AddOn $addOn = null)
	{
		$this->addOn = $addOn;
		$this->xmlExporter->setAddOn($addOn);
	}

	public function getAddOn()
	{
		return $this->addOn;
	}

	public function setIndependent(bool $independent)
	{
		$this->independent = $independent;
		$this->xmlExporter->setIndependent($independent);
	}

	public function getIndependent(): bool
	{
		return $this->independent;
	}

	protected function prepareFilesToCopy(string $buildRoot)
	{
		$assets = $this->getExportableAssets();
		if (!$assets)
		{
			return;
		}

		$uploadRoot = $buildRoot . \XF::$DS . self::UPLOAD_DIR;
		File::createDirectory($uploadRoot, false);

		$ds = \XF::$DS;
		$rootPath = \XF::getRootDirectory();

		foreach ($assets AS $key => $path)
		{
			if (preg_match('#^(https?://|/|\\\\)#i', $path))
			{
				continue;
			}

			if ($path === '')
			{
				continue;
			}

			if (strpos($path, 'data://') === 0)
			{
				$fs = $this->app->fs();

				try
				{
					$metadata = $fs->getMetadata($path);
				}
				catch (FilesystemException $e)
				{
					throw new PrintableException(\XF::phrase('file_or_directory_not_found_at_path_x', ['path' => $path]));
				}

				if (!$metadata)
				{
					continue;
				}

				if ($metadata['type'] == 'dir')
				{
					$contents = $fs->listContents($path, true);

					foreach ($contents AS $file)
					{
						if ($file['type'] == 'dir')
						{
							continue;
						}

						$abstractedPath = 'data://' . $file['path'];

						$filePath = File::copyAbstractedPathToTempFile($abstractedPath);
						$stdPath = $this->stripDataStylesPathPrefix($abstractedPath);
						File::copyFile($filePath, $uploadRoot . $ds . $stdPath, false);
					}
				}
				else
				{
					$filePath = File::copyAbstractedPathToTempFile($path);
					$stdPath = $this->stripDataStylesPathPrefix($path);
					File::copyFile($filePath, $uploadRoot . $ds . $stdPath, false);
				}

				$newAssetPath = $this->stripDataStylesPathPrefix($path);
				if ($newAssetPath !== $path)
				{
					$this->xmlExporter->addChangedAsset($key, $newAssetPath);
				}
			}
			else
			{
				$filePath = $rootPath . $ds . $path;

				if (!file_exists($filePath))
				{
					throw new PrintableException(\XF::phrase('file_or_directory_not_found_at_path_x', ['path' => $path]));
				}

				if (is_dir($filePath))
				{
					$files = File::getRecursiveDirectoryIterator($filePath);
					foreach ($files AS $file)
					{
						$stdPath = File::stripRootPathPrefix($file->getPathname(), $rootPath);
						if (!$file->isDir())
						{
							File::copyFile($file->getPathname(), $uploadRoot . $ds . $stdPath, false);
						}
					}
				}
				else
				{
					$stdPath = File::stripRootPathPrefix($filePath, $rootPath);
					File::copyFile($filePath, $uploadRoot . $ds . $stdPath, false);
				}
			}
		}
	}

	protected function validateContents(string $buildRoot, &$errors = []): bool
	{
		$uploadRoot = $buildRoot . \XF::$DS . self::UPLOAD_DIR;
		if (!file_exists($uploadRoot))
		{
			return true;
		}

		$validator = $this->service(ArchiveValidatorService::class, $uploadRoot, 'export');
		return $validator->validate($errors);
	}

	protected function stripDataStylesPathPrefix(string $abstractedPath): string
	{
		return $this->xmlExporter->stripDataStylesPathPrefix($abstractedPath);
	}

	protected function getExportableAssets(): array
	{
		$style = $this->style;

		if ($this->independent)
		{
			return $style->effective_assets;
		}
		else
		{
			return $style->assets;
		}
	}

	/**
	 * @return string Temp path to zip file (file will be deleted at end of request)
	 *
	 * @throws PrintableException
	 */
	public function build()
	{
		$buildRoot = File::createTempDir();

		$this->prepareFilesToCopy($buildRoot);

		if (!$this->validateContents($buildRoot, $errors))
		{
			throw new PrintableException($errors);
		}

		$zipTargetFile = File::getTempFile();
		$zipArchive = $this->initializeZipFile($zipTargetFile);

		File::writeFile(
			$buildRoot . \XF::$DS . self::XML_FILE_NAME,
			$this->xmlExporter->exportToXml()->saveXML()
		);

		$buildIterator = File::getRecursiveDirectoryIterator($buildRoot);
		foreach ($buildIterator AS $file)
		{
			// skip hidden dot files, e.g. .DS_Store, .gitignore etc.
			if ($this->isExcludedFileName($file->getBasename()))
			{
				continue;
			}

			$localName = str_replace('\\', '/', substr($file->getPathname(), strlen($buildRoot) + 1));

			if ($file->isDir())
			{
				$localName .= '/';
				$zipArchive->addEmptyDir($localName);
				$perm = 040755 << 16; // dir: 0755
			}
			else
			{
				$zipArchive->addFile($file->getPathname(), $localName);
				$perm = 0100644 << 16; // file: 0644
			}

			if (method_exists($zipArchive, 'setExternalAttributesName'))
			{
				$zipArchive->setExternalAttributesName($localName, \ZipArchive::OPSYS_UNIX, $perm);
			}
		}

		if (!$zipArchive->close())
		{
			throw new PrintableException($zipArchive->getStatusString());
		}

		return $zipTargetFile;
	}

	/**
	 * @param string $targetFile
	 *
	 * @return \ZipArchive
	 */
	protected function initializeZipFile(string $targetFile): \ZipArchive
	{
		$zipArchive = new \ZipArchive();
		if (($error = $zipArchive->open($targetFile, \ZipArchive::OVERWRITE)) !== true)
		{
			throw new \RuntimeException(sprintf("Unable to create zip archive, error %d", $error));
		}

		return $zipArchive;
	}

	protected function isExcludedFileName($fileName): bool
	{
		if ($fileName === '' || $fileName === false || $fileName === null)
		{
			return true;
		}

		if ($fileName[0] == '.' && $fileName != '.htaccess')
		{
			return true;
		}

		return false;
	}

	public function getArchiveFileName(): string
	{
		$styleFileName = $this->xmlExporter->getExportFileName();
		return preg_replace('/\.xml$/', '.zip', $styleFileName);
	}
}
