#!/usr/bin/env node
'use strict';

const fs = require('fs');
const path = require('path');
const glob = require('glob');
const winston = require('winston');
const { Validator } = require('jsonschema');
const dupKeyValidator = require('json-dup-key-validator');
const moment = require('moment-timezone');
const isEqual = require('lodash/isEqual');

const log = winston.createLogger({
  transports: [new winston.transports.Console()],
  format: winston.format.combine(
    winston.format.timestamp({ format: 'HH:mm:ss' }),
    winston.format.colorize({ error: true, warning: true }),
    winston.format.printf(info => `[${info.timestamp}] ${info.level}: ${info.message}`)
  )
});

/**
 * Validate a JSON file.
 * @param {string} filePath Absolute path to the file
 * @param {jsonschema.Validator} validator Validator object
 * @returns {(string|null)} Error if failed, null if passed
 */
const validateJSON = (filePath, validator) => {
  const data = fs.readFileSync(filePath, 'utf-8');

  const isInvalid = dupKeyValidator.validate(data, false);
  if (isInvalid !== undefined) {
    return isInvalid.replace(/\n/g, '\\n');
  }

  const obj = JSON.parse(data);
  const results = validator.validate(obj, '/');

  if (results.errors.length > 0) {
    const messageList = results.errors.map(result => String(result));
    return messageList.join('\n');
  }

  return null;
};

/**
 * Validate the scene exceptions JSON files.
 * @returns {boolean} Validation result.
 */
const validateSceneExceptions = () => {
  const validator = new Validator();
  validator.addSchema({
    'id': '/indexerSchema',
    'type': 'object',
    'additionalProperties': false,
    'patternProperties': {
      '^([1-9]\\d+)$': {
        'type': 'object',
        'additionalProperties': false,
        'patternProperties': {
          '^(-1|\\d+)$': {
            'type': 'array',
            'minItems': 1,
            'uniqueItems': true,
            'items': {
              'type': 'string'
            }
          }
        }
      }
    }
  });

  return glob
    .sync('scene_exceptions/scene_exceptions_*.json', {
      cwd: __dirname,
      absolute: true
    }).map(filePath => {
      const fileName = path.basename(filePath);
      log.info(`Validating: scene_exceptions/${fileName}`);

      // Example: scene_exceptions_tvdb.json
      const indexer = fileName.split('.')[0].split('_').pop();
      validator.addSchema({
        'id': '/',
        'type': 'object',
        'additionalProperties': false,
        'properties': {
          [indexer]: {
            '$ref': '/indexerSchema'
          }
        }
      });

      const result = validateJSON(filePath, validator);
      if (result !== null) {
        log.error(`${result}\n`);
        return false;
      }

      console.log();
      return true;
    }).every(Boolean);
};

/**
 * Validate the scene exceptions JSON files.
 * @returns {boolean} Validation result.
 */
const validateBrokenProviders = () => {
  const relFile = 'providers/broken_providers.json';
  const filePath = path.resolve(__dirname, relFile);
  log.info(`Validating: ${relFile}`);

  const validator = new Validator();
  validator.addSchema({
    'id': '/',
    'type': 'array',
    'minItems': 1,
    'uniqueItems': true,
    'items': {
      'type': 'string'
    }
  });

  const result = validateJSON(filePath, validator);
  if (result !== null) {
    log.error(`${result}\n`);
    return false;
  }

  console.log();
  return true;
};

/**
 * Validate the network timezones list.
 * @returns {boolean} Validation result.
 */
const validateNetworkTimezones = () => {
  const relFile = 'sb_network_timezones/network_timezones.txt';
  const filePath = path.resolve(__dirname, relFile);
  log.info(`Validating: ${relFile}`);

  let errors = 0;
  const data = fs.readFileSync(filePath, 'utf-8');
  const linePattern = /^.+:[\w/]+$/;

  // https://stackoverflow.com/a/5202185/7597273
  const rsplit = (str, sep, maxsplit) => {
    const split = str.split(sep);
    return maxsplit ?
      [split.slice(0, -maxsplit).join(sep)].concat(split.slice(-maxsplit)) :
      split;
  };

  const logError = (message, index, line) => {
    if (line) {
      log.error(`Line #${index}: ${message}\n\t\`${line}\``);
    } else {
      log.error(`Line #${index}: ${message}`);
    }
    errors++;
  };

  const timezones = moment.tz.names();
  const lines = data.split('\n');

  if (!data.trim() || lines.length === 0) {
    log.warn('File is empty.');
    return true;
  }

  lines.forEach((line, index) => {
    const lineIndex = index + 1;
    const trimmedLine = line.trim();

    if (lineIndex === lines.length) {
      if (trimmedLine) {
        logError('Please leave one empty line at the end of the file.', lineIndex);
      } else {
        // Last line should not be processed.
        return;
      }
    }

    if (!trimmedLine) {
      logError('Please remove empty lines.', lineIndex);
      return;
    }

    if (line.includes('\r')) {
      logError('`\\r` found - please only use Linux EOL (`\\n`).', lineIndex);
    }

    if (!linePattern.test(line)) {
      logError('Line format invalid, please use: `Network Name:Timezone`', lineIndex, line);
    }

    const timezone = rsplit(line, ':', 1)[1];
    if (!timezones.includes(timezone)) {
      logError(`Timezone \`${timezone}\` is invalid.`, lineIndex, line);
    }
  });

  const sortedLines = lines.slice().filter(Boolean);
  sortedLines.sort();
  sortedLines.push('');

  const sorted = isEqual(lines, sortedLines);
  if (!sorted) {
    if (process.argv.includes('--fix')) {
      log.info(`Writing sorted lines to ${relFile}.`);
      fs.writeFileSync(filePath, sortedLines.join('\n'));
    } else {
      log.error('Lines are unsorted. Run `yarn validate --fix` to fix.');
    }
  }

  console.log();
  return errors === 0 && sorted;
};

const results = [
  validateSceneExceptions(),
  validateBrokenProviders(),
  validateNetworkTimezones()
];
process.exit(
  Number(
    !results.every(Boolean)
  )
);

@@ -4478,16 +4478,44 @@
 e;%0A  %7D%0A%0A
+  const networkNames = %7B%7D;%0A%0A
   lines.
@@ -5210,24 +5210,34 @@
 %7D%0A%0A    const
+ %5Bnetwork,
  timezone = 
@@ -5233,16 +5233,17 @@
 timezone
+%5D
  = rspli
@@ -5261,11 +5261,8 @@
 , 1)
-%5B1%5D
 ;%0A  
@@ -5374,24 +5374,280 @@
 line);%0A    %7D
+%0A%0A    if (networkNames%5Bnetwork%5D === undefined) %7B%0A      networkNames%5Bnetwork%5D = lineIndex;%0A    %7D else %7B%0A      logError(%60Duplicate entry found.%60, lineIndex, line);%0A      logError(%60First entry.%60, networkNames%5Bnetwork%5D, lines%5BnetworkNames%5Bnetwork%5D - 1%5D);%0A    %7D
 %0A  %7D);%0A%0A  co

Validate that there are no duplicate networks on the list