<?php

namespace SilverStripe\Admin;

use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Core\Config\Config;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\ORM\DataObject;
use SilverStripe\Security\SecurityToken;
use SilverStripe\View\ArrayData;
use InvalidArgumentException;
use Translatable;

/**
 * Special request handler for admin/batchaction
 */
class CMSBatchActionHandler extends RequestHandler {

	/** @config */
	private static $batch_actions = array();

	private static $url_handlers = array(
		'$BatchAction/applicablepages' => 'handleApplicablePages',
		'$BatchAction/confirmation' => 'handleConfirmation',
		'$BatchAction' => 'handleBatchAction',
	);

	private static $allowed_actions = array(
		'handleBatchAction',
		'handleApplicablePages',
		'handleConfirmation',
	);

	/**
	 * @var Controller
	 */
	protected $parentController;

	/**
	 * @var String
	 */
	protected $urlSegment;

	/**
	 * @var String $recordClass The classname that should be affected
	 * by any batch changes. Needs to be set in the actual {@link CMSBatchAction}
	 * implementations as well.
	 */
	protected $recordClass = SiteTree::class;

	/**
	 * @param Controller $parentController
	 * @param string $urlSegment
	 * @param string $recordClass
	 */
	public function __construct($parentController, $urlSegment, $recordClass = null) {
		$this->parentController = $parentController;
		$this->urlSegment = $urlSegment;
		if($recordClass) {
			$this->recordClass = $recordClass;
		}

		parent::__construct();
	}

	/**
	 * Register a new batch action.  Each batch action needs to be represented by a subclass
	 * of {@link CMSBatchAction}.
	 *
	 * @param string $urlSegment The URL Segment of the batch action - the URL used to process this
	 * action will be admin/pages/batchactions/(urlSegment)
	 * @param string $batchActionClass The name of the CMSBatchAction subclass to register
	 * @param string $recordClass
	 */
	public static function register($urlSegment, $batchActionClass, $recordClass = SiteTree::class) {
		if(!is_subclass_of($batchActionClass, CMSBatchAction::class)) {
			throw new InvalidArgumentException(
				"CMSBatchActionHandler::register() - Bad class '$batchActionClass'"
			);
		}

		Config::inst()->update(
			CMSBatchActionHandler::class,
			'batch_actions',
			array(
				$urlSegment => array(
					'class' => $batchActionClass,
					'recordClass' => $recordClass
				)
			)
		);
	}

	public function Link() {
		return Controller::join_links($this->parentController->Link(), $this->urlSegment);
	}

	/**
	 * Invoke a batch action
	 *
	 * @param HTTPRequest $request
	 * @return HTTPResponse
	 */
	public function handleBatchAction($request) {
		// This method can't be called without ajax.
		if(!$request->isAjax()) {
			return $this->parentController->redirectBack();
		}

		// Protect against CSRF on destructive action
		if(!SecurityToken::inst()->checkRequest($request)) {
			return $this->httpError(400);
		}

		// Find the action handler
		$action = $request->param('BatchAction');
		$actionHandler = $this->actionByName($action);

		// Sanitise ID list and query the database for apges
		$csvIDs = $request->requestVar('csvIDs');
		$ids = $this->cleanIDs($csvIDs);

		// Filter ids by those which are applicable to this action
		// Enforces front end filter in LeftAndMain.BatchActions.js:refreshSelected
		$ids = $actionHandler->applicablePages($ids);

		// Query ids and pass to action to process
		$pages = $this->getPages($ids);
		return $actionHandler->run($pages);
	}

	/**
	 * Respond with the list of applicable pages for a given filter
	 *
	 * @param HTTPRequest $request
	 * @return HTTPResponse
	 */
	public function handleApplicablePages($request) {
		// Find the action handler
		$action = $request->param('BatchAction');
		$actionHandler = $this->actionByName($action);

		// Sanitise ID list and query the database for apges
		$csvIDs = $request->requestVar('csvIDs');
		$ids = $this->cleanIDs($csvIDs);

		// Filter by applicable pages
		if($ids) {
			$applicableIDs = $actionHandler->applicablePages($ids);
		} else {
			$applicableIDs = array();
		}

		$response = new HTTPResponse(json_encode($applicableIDs));
		$response->addHeader("Content-type", "application/json");
		return $response;
	}

	/**
	 * Check if this action has a confirmation step
	 *
	 * @param HTTPRequest $request
	 * @return HTTPResponse
	 */
	public function handleConfirmation($request) {
		// Find the action handler
		$action = $request->param('BatchAction');
		$actionHandler = $this->actionByName($action);

		// Sanitise ID list and query the database for apges
		$csvIDs = $request->requestVar('csvIDs');
		$ids = $this->cleanIDs($csvIDs);

		// Check dialog
		if($actionHandler->hasMethod('confirmationDialog')) {
			$response = new HTTPResponse(json_encode($actionHandler->confirmationDialog($ids)));
		} else {
			$response = new HTTPResponse(json_encode(array('alert' => false)));
		}

		$response->addHeader("Content-type", "application/json");
		return $response;
	}

	/**
	 * Get an action for a given name
	 *
	 * @param string $name Name of the action
	 * @return CMSBatchAction An instance of the given batch action
	 * @throws InvalidArgumentException if invalid action name is passed.
	 */
	protected function actionByName($name) {
		// Find the action handler
		$actions = $this->batchActions();
		if(!isset($actions[$name]['class'])) {
			throw new InvalidArgumentException("Invalid batch action: {$name}");
		}
		return $this->buildAction($actions[$name]['class']);
	}

	/**
	 * Return a SS_List of ArrayData objects containing the following pieces of info
	 * about each batch action:
	 *  - Link
	 *  - Title
	 *
	 * @return ArrayList
	 */
	public function batchActionList() {
		$actions = $this->batchActions();
		$actionList = new ArrayList();

		foreach($actions as $urlSegment => $action) {
			$actionObj = $this->buildAction($action['class']);
			if($actionObj->canView()) {
				$actionDef = new ArrayData(array(
					"Link" => Controller::join_links($this->Link(), $urlSegment),
					"Title" => $actionObj->getActionTitle(),
				));
				$actionList->push($actionDef);
			}
		}

		return $actionList;
	}

	/**
	 * Safely generate batch action object for a given classname
	 *
	 * @param string $class Class name to check
	 * @return CMSBatchAction An instance of the given batch action
	 * @throws InvalidArgumentException if invalid action class is passed.
	 */
	protected function buildAction($class) {
		if(!is_subclass_of($class, CMSBatchAction::class)) {
			throw new InvalidArgumentException("{$class} is not a valid subclass of CMSBatchAction");
		}
		return CMSBatchAction::singleton($class);
	}

	/**
	 * Sanitise ID list from string input
	 *
	 * @param string $csvIDs
	 * @return array List of IDs as ints
	 */
	protected function cleanIDs($csvIDs) {
		$ids = preg_split('/ *, */', trim($csvIDs));
		foreach($ids as $k => $id) {
			$ids[$k] = (int)$id;
		}
		return array_filter($ids);
	}

	/**
	 * Get all registered actions through the static defaults set by {@link register()}.
	 * Filters for the currently set {@link recordClass}.
	 *
	 * @return array See {@link register()} for the returned format.
	 */
	public function batchActions() {
		$actions = $this->config()->batch_actions;
		$recordClass = $this->recordClass;
		$actions = array_filter($actions, function($action) use ($recordClass) {
			return $action['recordClass'] === $recordClass;
		});
		return $actions;
	}

	/**
	 * Safely query and return all pages queried
	 *
	 * @param array $ids
	 * @return SS_List
	 */
	protected function getPages($ids) {
		// Check empty set
		if(empty($ids)) {
			return new ArrayList();
		}

		$recordClass = $this->recordClass;

		// Bypass translatable filter
		if(class_exists('Translatable') && $recordClass::has_extension('Translatable')) {
			Translatable::disable_locale_filter();
		}

		// Bypass versioned filter
		if($recordClass::has_extension(Versioned::class)) {
			// Workaround for get_including_deleted not supporting byIDs filter very well
			// Ensure we select both stage / live records
			$pages = Versioned::get_including_deleted($recordClass, array(
				'"RecordID" IN ('.DB::placeholders($ids).')' => $ids
			));
		} else {
			$pages = DataObject::get($recordClass)->byIDs($ids);
		}

		if(class_exists('Translatable') && $recordClass::has_extension('Translatable')) {
			Translatable::enable_locale_filter();
		}

		return $pages;
	}

}