<?php
/**
 * COmanage Match App Controller
 *
 * Portions licensed to the University Corporation for Advanced Internet
 * Development, Inc. ("UCAID") under one or more contributor license agreements.
 * See the NOTICE file distributed with this work for additional information
 * regarding copyright ownership.
 *
 * UCAID licenses this file to you under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with the
 * License. You may obtain a copy of the License at:
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * @link          http://www.internet2.edu/comanage COmanage Project
 * @package       match
 * @since         COmanage Match v1.0.0
 * @license       Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
 */

declare(strict_types = 1);

namespace App\Controller;

use Cake\Controller\Controller;
use Cake\Datasource\Exception;
use Cake\Datasource\Exception\RecordNotFoundException;
use Cake\Event\EventInterface;
use InvalidArgumentException;

class AppController extends Controller {
  // If set, the current requested Matchgrid ID. Note this may be *unauthenticated*
  // and so should not be trusted without further authorization.
  protected $cur_mg = null;
  
  // If set, the current primary link.
  protected $cur_pl = null;
  
  /**
   * Initialization callback.
   *
   * @since  COmanage Match v1.0.0
   */
  
  public function initialize(): void {
    parent::initialize();
    
    // Load Components used by most or all controllers
    
    $this->loadComponent('RequestHandler', [
      // As of Cake v3.6.7 need to disable this to suppress v4.0.0 deprecation warnings.
      'enableBeforeRedirect' => false
    ]);
    
    $this->loadComponent('Flash');
    
    $this->loadComponent('Auth', [
      // We want to use isAuthorized in each controller for request authorization
      'authorize' => [
        'Controller'
      ],
      // This corresponds to EnvAuthenticate
      'authenticate' => [ 
        'Env'
      ],
    ]);
    
    /*
     * Enable the following components for recommended CakePHP security settings.
     * see https://book.cakephp.org/3.0/en/controllers/components/security.html
     */
    $this->loadComponent('Security');
    
    // CSRF Protection is enabled via in Middleware via Application.php.
    
    // This is the COmanage AuthorizationComponent, not to be confused with
    // Cake's AuthComponent, or the use of Controller Authorization.
    $this->loadComponent('Authorization');
  }
  
  /**
   * Callback run prior to the request action.
   *
   * @since  COmanage Match v1.0.0
   * @param \Cake\Event\EventInterface $event Cake Event
   */
  
  public function beforeFilter(EventInterface $event) {
    parent::beforeFilter($event);

    // Determine the timezone
    $this->setTZ();
    
    // Determine the requested Matchgrid
    $this->setMatchgrid();
  }
  
  /**
   * Callback run prior to the view rendering.
   *
   * @since  COmanage Match v1.0.0
   * @param \Cake\Event\EventInterface $event Cake Event
   */
    
  public function beforeRender(EventInterface $event) {
    parent::beforeRender($event);
    
    // The current user, if authenticated
    $curUser = $this->request->getSession()->read('Auth.User');
    $this->set('vv_user', $curUser);
    
    // The current Matchgrid, as determined in beforeFilter()
    $mgid = null;
    
    if($this->cur_mg) {
      $mgid = $this->cur_mg->id;
    }
    
    // Available Matchgrids
    $this->Matchgrids = $this->fetchTable('Matchgrids');
    $this->set('vv_matchgrids', $this->Matchgrids->find('all')->find('activeMatchGrids')->order(['table_name' => 'ASC'])->toArray());
    
    // The set of menu permissions, so the layout knows what to render
    if(isset($this->Authorization) && $curUser) {
      // Ordinarily $this->Authorization will be set, but under certain error conditions
      // it won't, which will prevent error messages from rendering
      
      $this->set('vv_menu_permissions',
                 $this->Authorization->menuPermissions($this->request->getSession()->read('Auth.User.username'), $mgid));
    }
  }

  /**
   * Obtain information about the Standard Object's Primary Link, if set.
   * The $vv_primary_link view variable is also set.
   *
   * @since  COmanage Match v1.0.0
   * @param  boolean $lookup If true, get the value of the primary link, not just the attribute
   * @return array           Array holding the primary link attribute, and optionally its value
   * @throws \RuntimeException
   */
  
  protected function getPrimaryLink(bool $lookup=false) {
    // Did we already figure this out? (But only if $lookup)
    if($lookup && isset($this->cur_pl['linkvalue'])) {
      return $this->cur_pl;
    }
    
    // $this->name = Models
    $modelsName = $this->name;
    // $modelName = Model
    $modelName = \Cake\Utility\Inflector::singularize($this->name);
    
    $this->cur_pl = [];
    
    // PrimaryLinkTrait
    if(method_exists($this->$modelsName, "getPrimaryLink")
       && $this->$modelsName->getPrimaryLink()) {
      $this->cur_pl['linkattr'] = $this->$modelsName->getPrimaryLink();
      $this->set('vv_primary_link', $this->cur_pl['linkattr']);
      
      if($lookup) {
        // Try to find a value
        
        if($this->request->is('get')) {
          // If this action allows unkeyed, asserted primary link IDs, check the query
          // string (eg: 'add' or 'index' allow matchgrid_id to be passed in)
          if($this->$modelsName->allowUnkeyedPrimaryLink($this->request->getParam('action'))
             && $this->request->getQuery($this->cur_pl['linkattr'])) {
            $this->cur_pl['linkvalue'] = $this->request->getQuery($this->cur_pl['linkattr']);
          } elseif($this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action'))) {
            // Try to map the requested object ID
            $param = (int)$this->request->getParam('pass.0');
            
            if(!empty($param)) {
              $this->cur_pl['linkvalue'] = $this->$modelsName->calculatePrimaryLinkId($param);
            }
          }
        } elseif($this->request->is('post') || $this->request->is('put')) {
          // Look in the data for the primary link ID
          if(!empty($this->request->getData($this->cur_pl['linkattr']))) {
            $this->cur_pl['linkvalue'] = $this->request->getData($this->cur_pl['linkattr']);
          } elseif(!empty($this->request->getData($modelName . "." . $this->cur_pl['linkattr']))) {
            $this->cur_pl['linkvalue'] = $this->request->getData($modelName . "." . $this->cur_pl['linkattr']);
          } elseif($this->$modelsName->allowUnkeyedPrimaryLink($this->request->getParam('action'))
             && $this->request->getQuery($this->cur_pl['linkattr'])) {
            // If this action allows unkeyed, asserted primary link IDs, check the query
            // string (eg: 'add' or 'index' allow matchgrid_id to be passed in). Note we
            // do this even though we're in post/put.
            $this->cur_pl['linkvalue'] = $this->request->getQuery($this->cur_pl['linkattr']);
          } elseif($this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action'))) {
            // Try to map the requested object ID (this is probably a delete, so no attribute in post body)
            $param = (int)$this->request->getParam('pass.0');
            
            if(!empty($param)) {
              $this->cur_pl['linkvalue'] = $this->$modelsName->calculatePrimaryLinkId($param);
            }
          }
        }
        
        if(empty($this->cur_pl['linkvalue']) && !$this->$modelsName->allowEmptyPrimaryLink()) {
          throw new \RuntimeException(__('match.er.primary_link', [ $this->cur_pl['linkattr'] ]));
        }
      }
      
      if(!empty($this->cur_pl['linkvalue'])) {
        // Look up the link value to find the related entity
        
        $linkModelName = $this->$modelsName->getPrimaryLinkTableName();
        $linkModel = $this->getTableLocator()->get($linkModelName);
        
        $this->set('vv_primary_link_model', $linkModelName);
        $this->set('vv_primary_link_obj', $linkModel->findById($this->cur_pl['linkvalue'])->firstOrFail());
      }
    }
    
    return $this->cur_pl;
  }
  
  /**
   * Determine the (requested) current Matchgrid and make it available to the
   * rest of the application.
   *
   * @since  COmanage Match v1.0.0
   * @throws Cake\Datasource\Exception\RecordNotFoundException
   * @throws \InvalidArgumentException
   */
  
  protected function setMatchgrid() {
    // Note: TierApiController overrides this.
    
    // $this->name = Models
    $modelsName = $this->name;
    // $modelName = Model
    $modelName = \Cake\Utility\Inflector::singularize($this->name);
    
    if(!method_exists($this->$modelsName, "requiresMatchgrid")
       || !$this->$modelsName->requiresMatchgrid()) {
      // Nothing to do, matchgrid not required by this model/controller
      return;
    }
    
    // Not all models have matchgrid as their primary link. This will also
    // trigger setting of the viewVar for breadcrumbs and anything else.
    // PrimaryLinkTrait
    $link = $this->getPrimaryLink(true);
    
    // Try to find the requested matchgrid
    $mgid = null;
    
    // If this action allows unkeyed, asserted primary link IDs, check the query
    // string (eg: 'add' or 'index' allow matchgrid_id to be passed in), and
    // possibly dereference it if the primary key is not matchgrid_id.
    if($this->$modelsName->allowUnkeyedPrimaryLink($this->request->getParam('action'))) {
      if($link['linkattr'] == 'matchgrid_id') {
        // Simply accept the passed matchgrid ID
        if($this->request->is('get')) {
          $mgid = $this->request->getQuery('matchgrid_id');
        } elseif($this->request->is('post') || $this->request->is('put')) {
          if(!empty($this->request->getData('matchgrid_id'))) {
            $mgid = $this->request->getData('matchgrid_id');
          } elseif(!empty($this->request->getData($modelName . ".matchgrid_id"))) {
            $mgid = $this->request->getData($modelName . ".matchgrid_id");
          } elseif($this->name == 'MatchgridRecords'
                   && !empty($this->request->getQuery('matchgrid_id'))) {
            // As a special case for MatchgridRecords, we accept the matchgrid_id
            // as a get parameter even though the action is post/put, since this
            // is how it is provided for a delete action.
            $mgid = $this->request->getQuery('matchgrid_id');
          }
        }
      } else {
        // We already have the primary link object in a viewvar
        
        $ViewBuilder = $this->viewBuilder();
        $plObj = $ViewBuilder->getVar('vv_primary_link_obj');
        
        if(!empty($plObj->matchgrid_id)) {
          $mgid = $plObj->matchgrid_id;
        }
      }
    } elseif($this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action'))) {
      // Try to map the requested object ID
      $param = (int)$this->request->getParam('pass.0');
      
      if(!empty($param)) {
        $mgid = $this->$modelsName->calculateMatchgridId($param);
      }
    }
    
    if(!$mgid && !$this->$modelsName->allowEmptyMatchgrid()) {
      // If we get this far without a Matchgrid ID, something went wrong.
      throw new \RuntimeException(__('match.er.mgid'));
    }
    
    if($mgid) {
      $this->Matchgrids = $this->fetchTable('Matchgrids');
      
      // This throws Cake\Datasource\Exception\RecordNotFoundException which
      // we just let pass up the stack.
      $this->cur_mg = $this->Matchgrids->findById($mgid)->firstOrFail();
      $this->set('vv_cur_mg', $this->cur_mg);
    }
  }

  /**
   * Determine the current timezone and make it available to the
   * rest of the application.
   *
   * @since  COmanage Match v1.0.0
   */

  protected function setTZ() {
    // $this->name = Models
    $modelsName = $this->name;

    // See if we've collected it from the browser in a previous page load. Otherwise
    // use the system default. If the user set a preferred timezone, we'll catch that below.

    $tz = date_default_timezone_get();

    if(!empty($_COOKIE['cm_match_tz_auto'])) {
      // We have an auto-detected timezone from a previous page render from the browser.
      // Note we don't call date_default_timezone_set() because we still want to record
      // times internally in UTC (at the expense of having to convert back and forth).
      $tz = $_COOKIE['cm_match_tz_auto'];
    }
    
    $this->set('vv_tz', $tz);

    if($this->$modelsName->behaviors()->has('Timezone')) {
      // Tell TimezoneBehavior what the current timezone is
      $this->$modelsName->setTimeZone($tz);
    }
  }
}