/**
 * Function/Windows/scheduled-tasks.js
 * ---------------------------------------------------------------------------
 * SCHEDULED TASKS VIEWER
 *
 * The third place Windows starts programs behind your back - after services
 * and the startup folder. This lists every scheduled task with its state, what
 * it actually runs, when it last ran and whether that run failed.
 *
 * Enabling or disabling a task is an advanced action: administrator rights, an
 * explicit confirmation, blocked in Safe Mode and in Dry Run, and never
 * applied to a task that belongs to Windows itself.
 * ---------------------------------------------------------------------------
 */

'use strict';

const kit = require('../../core/kit');
const system = require('../../core/system');

/* --------------------------------------------------------------------------
 * Collection
 * ------------------------------------------------------------------------ */

/**
 * Primary source: the ScheduledTasks module (Windows 8 / Server 2012 and up).
 * Dates are formatted in PowerShell so the JSON never carries a locale.
 */
const QUERY = [
  '$ErrorActionPreference = "SilentlyContinue"',
  // 1999-11-30 is the Task Scheduler's "this never ran" sentinel date.
  '$stamp = { param($d) if ($d -and $d -gt [datetime]"2000-01-01") { $d.ToString("yyyy-MM-dd HH:mm") } else { $null } }',
  '$rows = foreach ($task in Get-ScheduledTask) {',
  '  $info = $task | Get-ScheduledTaskInfo',
  '  [pscustomobject]@{',
  '    Name        = $task.TaskName',
  '    Folder      = $task.TaskPath',
  '    State       = [string]$task.State',
  '    Author      = $task.Author',
  '    Description = $task.Description',
  '    RunAs       = $task.Principal.UserId',
  '    RunLevel    = [string]$task.Principal.RunLevel',
  '    Command     = (($task.Actions | ForEach-Object { (($_.Execute, $_.Arguments) -join " ").Trim() }) -join " ; ")',
  '    Triggers    = (($task.Triggers | ForEach-Object { ($_.CimClass.CimClassName -replace "^MSFT_Task", "" -replace "Trigger$", "") }) -join ", ")',
  '    LastRun     = & $stamp $info.LastRunTime',
  '    NextRun     = & $stamp $info.NextRunTime',
  '    LastResult  = $info.LastTaskResult',
  '    MissedRuns  = $info.NumberOfMissedRuns',
  '  }',
  '}',
  '@($rows) | ConvertTo-Json -Depth 3 -Compress',
].join('\n');

/** Fallback for systems without the ScheduledTasks module: schtasks.exe. */
const FALLBACK_QUERY = [
  '$ErrorActionPreference = "SilentlyContinue"',
  '$rows = schtasks /query /fo csv /v | ConvertFrom-Csv | Where-Object { $_.TaskName -and $_.TaskName -ne "TaskName" } | ForEach-Object {',
  '  [pscustomobject]@{',
  '    Name        = (Split-Path $_.TaskName -Leaf)',
  '    Folder      = (Split-Path $_.TaskName -Parent)',
  '    State       = $_.Status',
  '    Author      = $_.Author',
  '    Description = $_.Comment',
  '    RunAs       = $_."Run As User"',
  '    RunLevel    = $null',
  '    Command     = $_."Task To Run"',
  '    Triggers    = $_."Schedule Type"',
  '    LastRun     = $_."Last Run Time"',
  '    NextRun     = $_."Next Run Time"',
  '    LastResult  = $_."Last Result"',
  '    MissedRuns  = 0',
  '  }',
  '}',
  '@($rows) | ConvertTo-Json -Depth 3 -Compress',
].join('\n');

async function collect() {
  const primary = await system.runPowerShellJson(QUERY, { timeoutMs: 90000 });
  if (primary.ok) {
    const rows = system.asArray(primary.data);
    if (rows.length) return rows;
  }
  const fallback = await system.runPowerShellJson(FALLBACK_QUERY, { timeoutMs: 90000 });
  return fallback.ok ? system.asArray(fallback.data) : null;
}

/* --------------------------------------------------------------------------
 * Classification
 * ------------------------------------------------------------------------ */

/** The exit codes a healthy task reports. */
const HEALTHY_RESULTS = new Set([
  0, // success
  267009, // currently running
  267011, // has not run yet
  267014, // terminated by the user
  1, // the task ran; the program returned 1 - too common to call a failure
]);

/** Result codes worth translating into something readable. */
const RESULT_TEXT = {
  0: 'Success',
  1: 'Program returned 1',
  2: 'File not found',
  10: 'Environment is incorrect',
  267008: 'Ready',
  267009: 'Currently running',
  267010: 'Not scheduled',
  267011: 'Has not run yet',
  267014: 'Stopped by the user',
  2147750687: 'Already running - a new instance was refused',
  2147942402: 'The program was not found',
  2147942405: 'Access denied',
};

/** Tasks shipped with Windows live under \Microsoft\ - never touched here. */
function isWindowsTask(task) {
  const folder = String(task.Folder || '').replace(/\\/g, '/').toLowerCase();
  const author = String(system.clean(task.Author) || '').toLowerCase();
  return folder.startsWith('/microsoft/') || author.includes('microsoft corporation');
}

function resultCode(task) {
  const value = Number(task.LastResult);
  return Number.isFinite(value) ? value : null;
}

function hasFailed(task) {
  const code = resultCode(task);
  if (code === null) return false;
  return !HEALTHY_RESULTS.has(code);
}

function resultText(task) {
  const code = resultCode(task);
  if (code === null) return system.clean(task.LastResult) || 'N/A';
  if (RESULT_TEXT[code]) return `${RESULT_TEXT[code]} (${code})`;
  return `0x${(code >>> 0).toString(16).toUpperCase()} (${code})`;
}

function stateOf(task) {
  return String(system.clean(task.State) || 'Unknown');
}

function isDisabled(task) {
  return /^disabled$/i.test(stateOf(task));
}

function fullName(task) {
  const folder = String(task.Folder || '\\').replace(/\\+$/, '');
  return `${folder}\\${system.clean(task.Name) || ''}`.replace(/^\\+/, '\\');
}

/* --------------------------------------------------------------------------
 * Views
 * ------------------------------------------------------------------------ */

const FILTERS = [
  { id: 'third-party', label: 'Third-party tasks (not Windows)', test: (t) => !isWindowsTask(t) },
  { id: 'all', label: 'All tasks' },
  { id: 'windows', label: 'Windows tasks only', test: isWindowsTask },
  { id: 'running', label: 'Currently running', test: (t) => /running/i.test(stateOf(t)) },
  { id: 'ready', label: 'Ready (armed)', test: (t) => /ready/i.test(stateOf(t)) },
  { id: 'disabled', label: 'Disabled tasks', test: isDisabled },
  { id: 'failing', label: 'Tasks whose last run failed', test: hasFailed },
  { id: 'scheduled', label: 'Tasks with a next run time', test: (t) => Boolean(system.clean(t.NextRun)) },
  { id: 'search', label: 'Search by name or command' },
];

function stateCell(ui, task) {
  const state = stateOf(task);
  if (/running/i.test(state)) return ui.c.brightCyan(state);
  if (/ready/i.test(state)) return ui.c.brightGreen(state);
  if (/disabled/i.test(state)) return ui.c.gray(state);
  return ui.c.brightYellow(state);
}

function renderTasks(ui, tasks, limit) {
  kit.table(
    ['TASK', 'STATE', 'LAST RUN', 'RESULT'],
    tasks.map((task) => [
      ui.c.brightWhite(ui.truncate(system.clean(task.Name) || '-', 30)),
      stateCell(ui, task),
      ui.c.gray(system.clean(task.LastRun) || 'never'),
      hasFailed(task) ? ui.c.brightRed('failed') : ui.c.brightGreen('ok'),
    ]),
    { limit }
  );
}

/* --------------------------------------------------------------------------
 * Advanced: enable / disable one task
 * ------------------------------------------------------------------------ */

async function toggleTask(ctx, task) {
  const { ui, prompt } = ctx;
  const disabled = isDisabled(task);
  const action = disabled ? 'enable' : 'disable';
  const label = system.clean(task.Name) || fullName(task);

  if (ctx.safeMode) {
    ui.blank();
    ui.warning('Safe Mode is enabled - scheduled tasks cannot be changed.');
    return false;
  }
  if (isWindowsTask(task)) {
    ui.blank();
    ui.error(`"${label}" belongs to Windows and will not be touched by this tool.`);
    return false;
  }
  if (!ctx.isAdmin) {
    ui.blank();
    ui.warning('Enabling or disabling a scheduled task requires administrator rights.');
    return false;
  }
  if (ctx.dryRun) {
    ui.blank();
    ui.info(`Dry Run: "${label}" would be ${action}d. Nothing was changed.`);
    return false;
  }

  ui.blank();
  ui.warning(`A ${action}d task changes what Windows runs on its own from now on.`);
  const confirmed = await prompt.confirm(`${disabled ? 'Enable' : 'Disable'} "${label}"?`, { danger: !disabled });
  if (confirmed !== true) {
    ui.info('Nothing was changed.');
    return false;
  }

  const folder = String(task.Folder || '\\').replace(/"/g, '');
  const name = String(system.clean(task.Name) || '').replace(/"/g, '');
  const cmdlet = disabled ? 'Enable-ScheduledTask' : 'Disable-ScheduledTask';
  const script = [
    '$ErrorActionPreference = "Stop"',
    `try { ${cmdlet} -TaskName "${name}" -TaskPath "${folder}" | Out-Null; "OK" }`,
    'catch { "ERROR: " + $_.Exception.Message }',
  ].join('\n');

  const spin = ui.spinner(`${disabled ? 'Enabling' : 'Disabling'} ${label}...`);
  const result = await system.runPowerShell(script, { timeoutMs: 60000 });
  spin.stop();

  const text = String(result.stdout || '').trim();
  ui.blank();
  if (text.startsWith('OK')) {
    ui.ok(`Task ${action}d.`);
    ctx.logger.warn('scheduled-tasks', `Task ${action}d`, fullName(task));
    return true;
  }
  ui.error(`The task could not be ${action}d: ${text || result.stderr || 'unknown error'}`);
  return false;
}

/* --------------------------------------------------------------------------
 * Module
 * ------------------------------------------------------------------------ */

module.exports = {
  name: 'Scheduled Tasks Viewer',
  menuName: 'Scheduled Tasks',
  description: 'Lists scheduled tasks with state, command, last run and failures, with filters and export.',
  version: '1.0.0',
  author: 'InfinitySniper HFX',
  category: 'Windows',
  order: 25,
  requiredPermissions: 'normal',
  destructive: true,
  dependencies: [],
  minimumAppVersion: '1.0.0',
  permissions: ['system:read', 'wmi:query', 'task:control(confirmed, admin)'],
  tags: ['tasks', 'scheduler', 'startup', 'autorun', 'windows'],
  documentation:
    'The Task Scheduler is the third place Windows starts programs from, after the services and the ' +
    'startup folder, and the one people rarely look at. This function lists every task with its ' +
    'state, the command it actually runs, its trigger types, the account it runs as, the last and ' +
    'next run time and the result of that last run. The default view hides the hundreds of tasks ' +
    'Windows ships with, so what is left is what was added to this machine. Filters cover running, ' +
    'ready, disabled, scheduled and failing tasks, and there is a search across names and commands. ' +
    'Enabling or disabling a task is possible but treated as an advanced action: administrator ' +
    'rights and a confirmation are required, it is blocked in Safe Mode and in Dry Run, and tasks ' +
    'under \\Microsoft\\ are never modified.',

  async selfTest(ctx) {
    if (!ctx.system.IS_WINDOWS) return { ok: false, message: 'Windows only' };
    const tasks = await collect();
    return tasks && tasks.length
      ? { ok: true, message: `${tasks.length} task(s)` }
      : { ok: false, message: 'no scheduled task data' };
  },

  async execute(ctx) {
    const { ui, prompt } = ctx;
    kit.header('Scheduled Tasks', ['What Windows runs on a schedule, and whether it worked.']);
    if (!kit.requireWindows('The scheduled tasks viewer')) return { ok: false, reason: 'unsupported-platform' };

    const spin = ui.spinner('Reading the Task Scheduler...');
    let tasks = await collect();
    spin.stop();
    if (!tasks || !tasks.length) return kit.noData('scheduled task information');

    let view = tasks.filter((task) => !isWindowsTask(task));
    let title = `THIRD-PARTY TASKS (${view.length})`;

    for (;;) {
      const thirdParty = tasks.filter((task) => !isWindowsTask(task));
      const running = tasks.filter((task) => /running/i.test(stateOf(task)));
      const disabled = tasks.filter(isDisabled);
      const failing = tasks.filter(hasFailed);

      ui.blank();
      ui.section('SUMMARY');
      ui.blank();
      kit.kv('Total Tasks', system.formatNumber(tasks.length));
      kit.kv('Added To This Machine', system.formatNumber(thirdParty.length), 'brightCyan');
      kit.kv('Currently Running', system.formatNumber(running.length));
      kit.kv('Disabled', system.formatNumber(disabled.length));
      kit.kv(
        'Last Run Failed',
        system.formatNumber(failing.length),
        failing.length ? 'brightYellow' : 'brightGreen'
      );

      ui.blank();
      ui.section(title);
      ui.blank();
      if (view.length) renderTasks(ui, view, 20);
      else ui.println('  ' + ui.c.gray('No task matches this view.'));

      ui.blank();
      ui.menu(
        [
          { key: '1', label: 'Change the filter' },
          { key: '2', label: 'Show task details' },
          { key: '3', label: 'Export the current view' },
          { key: '4', label: 'Enable or disable a task (advanced)', color: 'brightYellow' },
          { key: '5', label: 'Refresh' },
          { separator: true },
          { key: '0', label: 'Back', color: 'gray' },
        ],
        { title: 'SCHEDULED TASKS' }
      );
      ui.blank();

      const answer = await prompt.select();
      if (!answer && prompt.isClosed()) break;
      if (answer === '0') break;

      switch (answer) {
        case '1': {
          const index = await prompt.choose('Which view?', FILTERS.map((f) => f.label));
          if (index === null) break;
          const filter = FILTERS[index];
          if (filter.id === 'search') {
            const term = (await prompt.text('Task name or command contains')).trim().toLowerCase();
            if (!term) break;
            view = tasks.filter(
              (task) =>
                String(system.clean(task.Name) || '').toLowerCase().includes(term) ||
                String(system.clean(task.Command) || '').toLowerCase().includes(term) ||
                String(task.Folder || '').toLowerCase().includes(term)
            );
            title = `SEARCH: ${term.toUpperCase()} (${view.length})`;
          } else {
            view = filter.test ? tasks.filter(filter.test) : tasks;
            title = `${filter.label.toUpperCase()} (${view.length})`;
          }
          break;
        }

        case '2': {
          if (!view.length) {
            ui.warning('There is nothing to show in this view.');
            await prompt.pause();
            break;
          }
          const index = await prompt.choose(
            'Which task?',
            view.slice(0, 30).map((task) => ({
              label: system.clean(task.Name) || fullName(task),
              hint: `${stateOf(task)}${hasFailed(task) ? ' / failed' : ''}`,
            }))
          );
          if (index === null) break;
          const task = view[index];

          ui.blank();
          ui.section(String(system.clean(task.Name) || 'TASK').toUpperCase());
          ui.blank();
          kit.kv('Full Path', fullName(task));
          kit.kv('State', stateOf(task), isDisabled(task) ? 'gray' : 'brightWhite');
          kit.kv('Author', system.clean(task.Author));
          kit.kv('Runs As', system.clean(task.RunAs));
          kit.kv('Privileges', /highest/i.test(String(task.RunLevel)) ? 'Highest (elevated)' : 'Standard');
          kit.kv('Triggers', system.clean(task.Triggers));
          kit.kv('Last Run', system.clean(task.LastRun) || 'never');
          kit.kv('Next Run', system.clean(task.NextRun) || 'not scheduled');
          kit.kv('Last Result', resultText(task), hasFailed(task) ? 'brightRed' : 'brightGreen');
          kit.kv('Missed Runs', system.formatNumber(Number(task.MissedRuns) || 0));
          kit.kv('Belongs To Windows', isWindowsTask(task) ? 'Yes' : 'No');

          const command = system.clean(task.Command);
          if (command) {
            ui.blank();
            ui.println('  ' + ui.c.gray('Runs:'));
            for (const line of ui.wrap(command, ui.getWidth() - 4)) {
              ui.println('  ' + ui.c.white(line));
            }
          }
          const description = system.clean(task.Description);
          if (description) {
            ui.blank();
            for (const line of ui.wrap(description, ui.getWidth() - 4)) {
              ui.println('  ' + ui.c.gray(line));
            }
          }
          await prompt.pause();
          break;
        }

        case '3': {
          const report = kit.report('Scheduled Tasks Report');
          const summary = report.section('SUMMARY');
          summary.add('Total Tasks', system.formatNumber(tasks.length));
          summary.add('Added To This Machine', system.formatNumber(thirdParty.length));
          summary.add('Currently Running', system.formatNumber(running.length));
          summary.add('Disabled', system.formatNumber(disabled.length));
          summary.add('Last Run Failed', system.formatNumber(failing.length));
          summary.add('View', title);

          const list = report.section('TASKS');
          view.forEach((task, index) => {
            list.add(
              `${index + 1}. ${fullName(task)}`,
              [
                stateOf(task),
                `last run ${system.clean(task.LastRun) || 'never'}`,
                resultText(task),
                system.clean(task.Command) || 'no command',
              ].join(' | ')
            );
          });
          kit.saveReport(report, 'ScheduledTasks');
          await prompt.pause();
          break;
        }

        case '4': {
          const changeable = view.filter((task) => !isWindowsTask(task));
          if (!changeable.length) {
            ui.blank();
            ui.warning('This view contains no task that may be changed (Windows tasks are protected).');
            await prompt.pause();
            break;
          }
          const index = await prompt.choose(
            'Which task?',
            changeable.slice(0, 30).map((task) => ({
              label: system.clean(task.Name) || fullName(task),
              hint: isDisabled(task) ? 'disabled - can be enabled' : 'enabled - can be disabled',
            }))
          );
          if (index === null) break;

          const changed = await toggleTask(ctx, changeable[index]);
          if (changed) {
            const refresh = ui.spinner('Refreshing scheduled tasks...');
            tasks = (await collect()) || tasks;
            refresh.stop();
            view = tasks.filter((task) => !isWindowsTask(task));
            title = `THIRD-PARTY TASKS (${view.length})`;
          }
          await prompt.pause();
          break;
        }

        case '5': {
          const refresh = ui.spinner('Refreshing scheduled tasks...');
          tasks = (await collect()) || tasks;
          refresh.stop();
          view = tasks.filter((task) => !isWindowsTask(task));
          title = `THIRD-PARTY TASKS (${view.length})`;
          break;
        }

        default:
          ui.warning('Enter one of the numbers shown above, or 0 to go back.');
          await prompt.pause();
      }
    }

    return kit.done('Scheduled tasks viewer closed.');
  },
};
