<?php

/**
 * @file
 * Module for importing LOM-CH descriptions.
 *
 * The Archibald Import module allows admins to import LOM-CH description data
 * along with revision information and other meta-data. This data can come from
 * exports, like the ones produced by Archibald Export, or even directly from
 * the API
 */

/**
 * Implements hook_menu().
 */
function archibald_import_menu() {
  $items = array();

  $items['admin/archibald/description/import'] = array(
    'title' => 'Import',
    'description' => 'Import Archibald descriptions.',
    'access arguments' => array('import archibald descriptions'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('archibald_import_upload_zip_form'),
    'file' => 'includes/archibald_import.pages.inc',
  );

  $items['admin/archibald/description/import/upload-zip'] = array(
    'title' => 'Upload ZIP',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );

  $items['archibald-import/replace/%node/%node'] = array(
    'title' => 'Replace node',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('archibald_import_replace_node_form', 2, 3),
    'access arguments' => array('import archibald descriptions'),
    'file' => 'includes/archibald_import.pages.inc',
    'type' => MENU_CALLBACK,
  );

  // We can only import from the catalog if partners have been configured.
  $partners = archibald_partner_load_all();
  if (!empty($partners)) {
    if (!$default_partner = archibald_load_default_partner()) {
      $default_partner = reset($partners);
    }

    $items['admin/archibald/description/import/catalog'] = array(
      'title' => 'Import from catalog',
      'access arguments' => array('import archibald descriptions'),
      'page callback' => 'drupal_get_form',
      'page arguments' => array('archibald_import_from_catalog_form', 5),
      'file' => 'includes/archibald_import.pages.inc',
      'type' => MENU_LOCAL_TASK,
      // Don't put it in the administration navigation. It looks odd... It's
      // a local task anyway (tab).
      'menu_name' => FALSE,
    );

    foreach ($partners as $i => $partner) {
      $items["admin/archibald/description/import/catalog/{$partner->id}"] = array(
        'title' => $partner->name,
        'access arguments' => array('import archibald descriptions'),
        'type' => $partner->id == $default_partner->id ? MENU_DEFAULT_LOCAL_TASK : MENU_LOCAL_TASK,
        'weight' => $partner->id == $default_partner->id ? -10 : $i,
        // Don't put it in the administration navigation. It looks odd... It's
        // a local task anyway (tab).
        'menu_name' => FALSE,
      );
    }
  }

  return $items;
}

/**
 * Implements hook_menu_alter().
 */
function archibald_import_menu_alter(&$items) {
  // Increase readability...
  $path = 'admin/archibald/description/unfinished-imports';
  if (isset($items[$path])) {
    // We do the same as in archibald_menu_alter().
    $items[$path]['title callback'] = '_archibald_title_callback';
    $items[$path]['title arguments'] = array(
      $items[$path]['title'],
      array(),
      array('context' => 'archibald:views'),
    );
  }
}

/**
 * Implements hook_permission().
 */
function archibald_import_permission() {
  return array(
    'import archibald descriptions' => array(
      'title' => t("Import Archibald descriptions"),
    ),
  );
}

/**
 * Implements hook_block_view_alter().
 */
function archibald_import_block_view_alter(&$data, $block) {
  if ($block->module == 'archibald' && $block->delta == 'admin_links') {
    if (!empty($data['content']['#links'])) {
      $item = menu_get_item('admin/archibald/description/import');
      if ($item['access']) {
        $data['content']['#links'][] = array(
          'title' => t("Import new descriptions"),
          'href' => 'admin/archibald/description/import',
        );
      }
    }
  }
}

/**
 * Implements hook_node_load().
 */
function archibald_import_node_load($nodes, $types) {
  if (in_array('archibald_lomch_description', $types)) {
    if (!empty($_POST['archibald_import_only_delete_local'])) {
      $nodes = array_map(function($node) {
        $node->only_delete_local = TRUE;
        return $node;
      }, $nodes);
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter() for node_delete_confirm().
 */
function archibald_import_form_node_delete_confirm_alter(&$form, $form_state) {
  // Add a new button to delete forms. Allow nodes to be only locally removed,
  // and kept on the API server.
  $node = $form['#node'];
  if ($node->type == 'archibald_lomch_description' && !empty($node->lom_id)) {
    $form['actions']['archibald_import_only_delete_local'] = array(
      '#type' => 'checkbox',
      '#title' => t("Delete locally only (not on the API)"),
      '#weight' => -10,
    );
  }
}

/**
 * Implements hook_form_FORM_ID_alter() for node_admin_content().
 */
function archibald_import_form_node_admin_content_alter(&$form, $form_state) {
  // Add a new button to delete forms. Allow nodes to be only locally removed,
  // and kept on the API server.
  $add_button = FALSE;
  $nodes = array();
  if (isset($form_state['values']['operation']) && $form_state['values']['operation'] == 'delete') {
    foreach ($form_state['values']['nodes'] as $nid) {
      $node = node_load($nid);
      if ($node->type == 'archibald_lomch_description' && !empty($node->lom_id)) {
        $add_button = TRUE;
        $nodes[$nid] = $node;
      }
    }
  }

  if ($add_button) {
    $form['archibald_lomch_description_nodes'] = array(
      '#prefix' => '<p><strong>' . t("The following items will also be removed from the API, unless you choose 'Delete locally only'.") . '</strong></p>',
      '#markup' => theme('item_list', array('items' => array_map(function($node) {
        return check_plain($node->title);
      }, $nodes))),
    );

    $form['actions']['archibald_import_only_delete_local'] = array(
      '#type' => 'checkbox',
      '#title' => t("Delete locally only (not on the API)"),
      '#weight' => -10,
    );
    $form['actions']['cancel']['#weight'] = 30;
  }
}

/**
 * Implements hook_archibald_assign_to_partner_action().
 */
function archibald_import_archibald_assign_to_partner_action($node, $context) {
  // This means we can remove any import errors that resulted from an incorrect
  // partner assignment. First, load the error log (if any).
  $log = archibald_import_load_error($node->nid);

  if (!empty($log['no_partner'])) {
    // Remove the partner issue, and update the log.
    $log['no_partner'] = FALSE;
    archibald_import_update_error($log);
    drupal_set_message(t("Solved the partner issue for the unfinished import %node.", array(
      '%node' => $node->title,
    )));
  }
}

/**
 * Implements hook_archibald_assign_to_catalog_action().
 */
function archibald_import_archibald_assign_to_catalog_action($node, $context) {
  // This means we can remove any import errors that resulted from an incorrect
  // catalog assignment. First, load the error log (if any).
  $log = archibald_import_load_error($node->nid);

  if (!empty($log['incorrect_catalog'])) {
    // Remove the catalog issue, and update the log.
    $log['incorrect_catalog'] = FALSE;
    archibald_import_update_error($log);
    drupal_set_message(t("Solved the catalog issue for the unfinished import %node.", array(
      '%node' => $node->title,
    )));
  }
}

/**
 * Implements hook_node_update().
 */
function archibald_import_node_update($node) {
  // Did this node have an import errors?
  $log = archibald_import_load_error($node->nid);
  if (!empty($log)) {
    if (!empty($log['no_user']) && $node->uid !== $node->original->uid) {
      // Remove the user issue, and update the log.
      $log['no_user'] = FALSE;
      drupal_set_message(t("Solved the user issue for the unfinished import %node.", array(
        '%node' => $node->title,
      )));
    }

    // Update the errors.
    archibald_import_update_error($log);
  }
}

/**
 * Implements hook_node_delete().
 */
function archibald_import_node_delete($node) {
  // Remove the entries from our import log.
  db_delete('archibald_import_errors')
    ->condition('nid', $node->nid)
    ->execute();

  // Check if this node had a LOM ID. If so, it may resolve a LOM ID conflict
  // we had.
  if (!empty($node->lom_id)) {
    // We simply solve the first one we get.
    $log = db_query("SELECT * FROM {archibald_import_errors} WHERE lom_id_conflict = :lom_id", array(
      ':lom_id' => $node->lom_id,
    ))->fetchAssoc();

    if (!empty($log)) {
      $log['lom_id_conflict'] = FALSE;
      archibald_import_update_error($log);

      // Assign the LOM ID to the imported node.
      db_insert('archibald_lom_id')
        ->fields(array(
          'nid' => $log['nid'],
          'lom_id' => $node->lom_id,
        ))
        ->execute();

      $imported_node = node_load($log['nid']);
      drupal_set_message(t("Solved the conflicting LOM ID issue for the unfinished import %node.", array(
        '%node' => $imported_node->title,
      )));
    }
  }
}

/**
 * Implements hook_entity_insert().
 */
function archibald_import_entity_insert($entity, $type) {
  // When a partner is updated, deleted or created, we must update the menu
  // cache, as we generate tabs in archibald_import_menu().
  if ($type == 'archibald_partner') {
    menu_rebuild();
  }
}

/**
 * Implements hook_entity_update().
 */
function archibald_import_entity_update($entity, $type) {
  archibald_import_entity_insert($entity, $type);
}

/**
 * Implements hook_entity_delete().
 */
function archibald_import_entity_delete($entity, $type) {
  archibald_import_entity_insert($entity, $type);
}

/**
 * Implements hook_views_api().
 */
function archibald_import_views_api() {
  return array(
    'api' => 3,
    'path' => drupal_get_path('module', 'archibald_import') . '/includes',
  );
}

/**
 * Implements hook_menu_local_tasks_alter().
 */
function archibald_import_menu_local_tasks_alter(&$data, $router_item, $root_path) {
  // On our description listing pages, add a link for importing descriptions.
  if (
      preg_match('/^admin\/archibald\/description(\/?|$)/', $root_path) &&
      !preg_match('/^admin\/archibald\/description\/import/', $root_path)
    ) {
    $item = menu_get_item('admin/archibald/description/import');
    if ($item['access']) {
      $item['title'] = t("Import descriptions");
      $data['actions']['output'][] = array(
        '#theme' => 'menu_local_action',
        '#link' => $item,
        '#weight' => 50,
      );
    }
  }
}

/**
 * Helper function for logging import errors.
 *
 * @param int $nid
 *    The node ID to store the error for.
 * @param string $no_user
 *    (optional) The error value for a missing user account. Defaults to null.
 * @param string $no_partner
 *    (optional) The error value for a missing partner. Defaults to null.
 * @param string $incorrect_catalog
 *    (optional) The error value for an incorrect catalog. Defaults to null.
 * @param string $lom_id_conflict
 *    (optional) The error value for a conflicting LOM ID. Defaults to null.
 */
function archibald_import_save_error($nid, $no_user = NULL, $no_partner = NULL, $incorrect_catalog = NULL, $lom_id_conflict = NULL) {
  db_insert('archibald_import_errors')
    ->fields(array(
      'nid' => $nid,
      'no_user' => $no_user,
      'no_partner' => $no_partner,
      'incorrect_catalog' => $incorrect_catalog,
      'lom_id_conflict' => $lom_id_conflict,
    ))
    ->execute();
}

/**
 * Helper function for loading an import error log.
 *
 * @param int $nid
 *    The node ID the log applies to.
 *
 * @return array|false
 *    The error log, or false if not found.
 */
function archibald_import_load_error($nid) {
  return db_query('SELECT * FROM {archibald_import_errors} WHERE nid = :nid', array(
    ':nid' => $nid,
  ))->fetchAssoc();
}

/**
 * Helper function for updating an error log.
 *
 * If no more errors are set on the log, it is removed.
 *
 * @param array $log
 *    An error log array, with the following keys:
 *    - nid: The node ID.
 *    - no_user: The username that wasn't found.
 *    - no_partner: The partner name that wasn't found.
 *    - incorrect_catalog: The catalog value that is incorrect.
 *    - lom_id_conflict: The conflicting lOM ID.
 */
function archibald_import_update_error($log) {
  foreach (array(
    'no_user',
    'no_partner',
    'incorrect_catalog',
    'lom_id_conflict',
  ) as $key) {
    // If any key is still set, we need to update.
    if (!empty($log[$key])) {
      db_update('archibald_import_errors')
        ->fields($log)
        ->condition('nid', $log['nid'])
        ->execute();
      return;
    }
  }

  // If we got here, it means there are no more errors. Remove it.
  db_delete('archibald_import_errors')
    ->condition('nid', $log['nid'])
    ->execute();
}

/**
 * Import a description in JSON format.
 *
 * @see _archibald_import_from_json()
 *
 * @param string $json
 *    The description, in valid LOM-CH JSON format.
 * @param string $meta_json
 *    (optional) The description meta data, in JSON format. Defaults to null.
 * @param string $revision_json
 *    (optional) The description revision information, in JSON format. Defaults
 *    to null.
 * @param array $files
 *    (optional) If any files were already pre-fetched, pass their references
 *    here. This is expected to be an array, where the keys are the paths in
 *    the JSON data, and the values local Drupal file IDs. Defaults to an empty
 *    array.
 *
 * @return object
 *    The node for the newly created description.
 */
function archibald_import_from_json($json, $meta_json = NULL, $revision_json = NULL, $files = array()) {
  global $user;
  module_load_include('inc', 'archibald_import', 'includes/archibald_import.import');

  watchdog('archibald_import', "Importing <pre>@json</pre>.<br />Meta-data: <pre>@meta_json</pre>.", array(
    '@json' => $json,
    '@meta_json' => $meta_json,
  ));

  // Prepare defaults.
  $meta_data = NULL;
  $account = $user;
  $catalogs = ARCHIBALD_CATALOG_BOTH;
  $partner = archibald_load_default_partner();

  // Prepare a list of error flags. Based on these, at the end, errors will be
  // stored, in order to flag this import as "unfinished".
  $error_no_user = FALSE;
  $error_no_partner = FALSE;
  $error_incorrect_catalog = FALSE;
  $error_lom_id_exists = FALSE;

  // Treat the meta data.
  if (!empty($meta_json)) {
    $meta_data = json_decode($meta_json, TRUE);

    if (!empty($meta_data['author'])) {
      if (!$account = user_load_by_mail($meta_data['author'])) {
        if (!$account = user_load_by_name($meta_data['author'])) {
          // Set it to the user that currently performs the import, and flag
          // it as an import error.
          $account = $user;
          $error_no_user = $meta_data['author'];
        }
      }
    }

    if (!empty($meta_data['lomId'])) {
      $lom_id = $meta_data['lomId'];
      if (archibald_lomch_description_load_by_lom_id($lom_id)) {
        // There already is a description with that LOM ID. Unset it, but flag
        // it as an import error.
        $error_lom_id_exists = $lom_id;
        $lom_id = NULL;
      }
    }

    if (!empty($meta_data['partner'])) {
      if (!$partner = archibald_partner_load_by_name($meta_data['partner'])) {
        // There is no such partner. Assign it to the default partner, but flag
        // it as an import error.
        $error_no_partner = $meta_data['partner'];
        $partner = archibald_load_default_partner();
      }
    }

    if (!empty($meta_data['catalogs'])) {
      $catalogs = $meta_data['catalogs'];
      $field = field_info_field('archibald_publication_catalogs');
      if (!in_array($catalogs, array_keys($field['settings']['allowed_values']))) {
        // Set it to the partner's own catalog for now, but flag it as an
        // import error.
        $error_incorrect_catalog = $catalogs;
        $catalogs = ARCHIBALD_CATALOG_PARTNER;
      }
    }
  }

  // Prepare the revision data.
  if (!empty($revision_json)) {
    $revisions_data = json_decode($revision_json, TRUE);
  }
  else {
    $revisions_data = NULL;
  }

  // Prepare the node object.
  $node = (object) array(
    'type' => 'archibald_lomch_description',
    'uid' => $account->uid,
    'log' => t("Imported by %user on !date.", array(
      '%user' => $user->name,
      '!date' => format_date(REQUEST_TIME),
    )),
    'status' => 0,
    'promote' => 0,
    'sticky' => 0,
    'archibald_publication_catalogs' => array(
      LANGUAGE_NONE => array(array('value' => $catalogs)),
    ),
    'archibald_publication_partner' => array(
      LANGUAGE_NONE => array(array('target_id' => $partner->id)),
    ),
  );

  // Import data, starting with the revisions, if any.
  if (!empty($revisions_data)) {
    $vids = array();
    $new_config = $orig_config = variable_get('node_options_archibald_lomch_description', array());
    if (in_array('revision_moderation', $new_config)) {
      unset($new_config[array_search('revision_moderation', $new_config)]);
    }
    variable_set('node_options_archibald_lomch_description', $new_config);

    // Sort by date, oldest first.
    usort($revisions_data, function($a, $b) {
      return strtotime($a['date']) > strtotime($b['date']) ? 1 : -1;
    });

    foreach ($revisions_data as $revision_data) {
      // Try to fetch the revision author. If we don't have it, simply assign
      // it to the "global" author.
      if (!empty($revision_data['author'])) {
        if (!$revision_account = user_load_by_mail($revision_data['author'])) {
          if (!$revision_account = user_load_by_name($revision_data['author'])) {
            // Set it to the "global" author.
            $revision_account = $account;
          }
        }
      }

      $node_revision = clone($node);
      module_load_include('php', 'simpletest', 'drupal_web_test_case');
      simpletest_verbose(print_r($node_revision, 1));
      $node_revision->revision = isset($node->nid);
      $node_revision->revision_uid = $revision_account->uid;
      if (!_archibald_import_from_json(
        $node_revision,
        $revision_data['description'],
        $files
      )) {
        if (!empty($orig_config)) {
          variable_set('node_options_archibald_lomch_description', $orig_config);
        }
        drupal_set_message(t("Import was aborted!"), 'error');
        return FALSE;
      }

      // Let other modules have a say in the import.
      $context = array(
        'json' => $json,
        'meta_json' => $meta_json,
        'revision_json' => $revision_json,
        'files' => $files,
      );
      drupal_alter('archibald_json_import_revision', $node_revision, $context);

      if (!empty($context['abort'])) {
        if (!empty($orig_config)) {
          variable_set('node_options_archibald_lomch_description', $orig_config);
        }
        drupal_set_message(t("Import was aborted!"), 'error');
        return FALSE;
      }

      // Save the revision.
      node_save($node_revision);

      // Store the node revision ID.
      $vids[] = $node_revision->vid;

      // If this is the first revision to be saved, we need to store the node
      // NID and VID.
      if (empty($node->nid)) {
        $node->nid = $node_revision->nid;
        $node->vid = $node_revision->vid;
      }
    }
  }

  // Finish the with the current version.
  $node->revision = isset($node->nid);
  $node->revision_uid = $node->uid;
  $node->is_pending = TRUE;
  if (!_archibald_import_from_json($node, json_decode($json, TRUE), $files)) {
    if (!empty($orig_config)) {
      variable_set('node_options_archibald_lomch_description', $orig_config);
    }
    drupal_set_message(t("Import was aborted!"), 'error');
    return FALSE;
  }

  // Let other modules have a say in the import.
  $context = array(
    'json' => $json,
    'meta_json' => $meta_json,
    'revision_json' => $revision_json,
    'files' => $files,
  );
  drupal_alter('archibald_json_import', $node, $context);

  if (!empty($context['abort'])) {
    if (!empty($orig_config)) {
      variable_set('node_options_archibald_lomch_description', $orig_config);
    }
    drupal_set_message(t("Import was aborted!"), 'error');
    return FALSE;
  }

  // Save the node.
  node_save($node);

  // If $orig_config is set, it means it as overridden for the revisions.
  // Restore it.
  if (!empty($orig_config)) {
    variable_set('node_options_archibald_lomch_description', $orig_config);
  }

  // In order to prevent confusion in the different UIs, we need to assign the
  // second-last revision as the "current" one. Otherwise, all revisions will be
  // marked as "archived" by the revisioning module, which will confuse users.
  // Fetch the second latest revision ID (last in our list, as we didn't store
  // the "current" one), and set it as the current revision. Note that this is
  // only necessary if we imported revisions.
  if (!empty($vids)) {
    db_update('node')
      ->fields(array('vid' => end($vids)))
      ->condition('nid', $node->nid)
      ->execute();
  }

  // Store the LOM ID, if there is any. This also means we publish the latest
  // revision.
  if (!empty($lom_id)) {
    db_insert('archibald_lom_id')
      ->fields(array(
        'nid' => $node->nid,
        'lom_id' => $lom_id,
      ))
      ->execute();

    // Publish this revision, but bypass the communication step with the API.
    module_load_include('inc', 'revisioning', 'revisioning_triggers_actions');
    $node->bypass_publishing = TRUE;
    revisioning_publish_latest_revision_action($node);
  }

  // If there are any import errors, log them.
  if (
    $error_no_user ||
    $error_no_partner ||
    $error_incorrect_catalog ||
    $error_lom_id_exists
  ) {
    archibald_import_save_error(
      $node->nid,
      $error_no_user,
      $error_no_partner,
      $error_incorrect_catalog,
      $error_lom_id_exists
    );

    drupal_set_message(t("An error occurred while importing %title. Please go to <a href='!link'>this page</a> to solve the issue.", array(
      '%title' => $node->title,
      '!link' => url('admin/archibald/description/unfinished-imports'),
    )), 'warning');
  }

  // Done.
  return $node;
}
