/**
 * Skill loader — discovers and loads skills from disk.
 *
 * Skills are loaded synchronously at startup from up to three directories
 * (bundled, managed, workspace), each mapped to a SkillTier.
 */

import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
import { resolve, join, basename } from 'path';
import { execSync } from 'child_process';
import { platform } from 'os';
import type { Skill, SkillManifest, SkillRequirements, SkillTier } from './types.js';
import { scanSkillDirectory } from './scanner.js';
import { auditLogger } from '../audit/index.js';

function isStringArray(value: unknown): value is string[] {
  return Array.isArray(value) && value.every((item) => typeof item === 'string');
}

function hasValidInstallers(manifest: unknown): boolean {
  if (!manifest || typeof manifest !== 'object') {
    return false;
  }

  const candidate = manifest as { installers?: unknown };
  if (candidate.installers === undefined) {
    return true;
  }

  if (!Array.isArray(candidate.installers)) {
    return false;
  }

  return candidate.installers.every((installer) => {
    if (!installer || typeof installer !== 'object') {
      return false;
    }

    const typedInstaller = installer as { type?: unknown; packages?: unknown; url?: unknown; destination?: unknown };
    if (typedInstaller.type === 'brew' || typedInstaller.type === 'node' || typedInstaller.type === 'go') {
      return isStringArray(typedInstaller.packages);
    }

    if (typedInstaller.type === 'download') {
      if (typeof typedInstaller.url !== 'string') {
        return false;
      }
      if (typedInstaller.destination !== undefined && typeof typedInstaller.destination !== 'string') {
        return false;
      }
      return true;
    }

    return false;
  });
}

function isNumberArray(value: unknown): value is number[] {
  return Array.isArray(value) && value.every((item) => typeof item === 'number' && Number.isFinite(item));
}

function hasValidPermissions(manifest: unknown): boolean {
  if (!manifest || typeof manifest !== 'object') {
    return false;
  }

  const candidate = manifest as { permissions?: unknown };
  if (candidate.permissions === undefined) {
    return true;
  }

  if (!candidate.permissions || typeof candidate.permissions !== 'object') {
    return false;
  }

  const perms = candidate.permissions as {
    tool_groups?: unknown;
    tools?: unknown;
    fs?: unknown;
    net?: unknown;
    secrets?: unknown;
    execution_environment?: unknown;
  };

  if (perms.tool_groups !== undefined && !isStringArray(perms.tool_groups)) {
    return false;
  }
  if (perms.tools !== undefined && !isStringArray(perms.tools)) {
    return false;
  }

  if (perms.fs !== undefined) {
    if (!perms.fs || typeof perms.fs !== 'object') {
      return false;
    }
    const fsPerms = perms.fs as { read?: unknown; write?: unknown };
    if (fsPerms.read !== undefined && !isStringArray(fsPerms.read)) {
      return false;
    }
    if (fsPerms.write !== undefined && !isStringArray(fsPerms.write)) {
      return false;
    }
  }

  if (perms.net !== undefined) {
    if (!Array.isArray(perms.net)) {
      return false;
    }
    for (const entry of perms.net) {
      if (!entry || typeof entry !== 'object') {
        return false;
      }
      const e = entry as { host?: unknown; ports?: unknown };
      if (typeof e.host !== 'string' || e.host.trim().length === 0) {
        return false;
      }
      if (e.ports !== undefined && !isNumberArray(e.ports)) {
        return false;
      }
    }
  }

  if (perms.secrets !== undefined && !isStringArray(perms.secrets)) {
    return false;
  }

  if (perms.execution_environment !== undefined) {
    if (perms.execution_environment !== 'sandbox' && perms.execution_environment !== 'host') {
      return false;
    }
  }

  return true;
}

/**
 * Check whether a skill's system requirements are met.
 *
 * Validates OS platform, binary availability (via `which`/`where`),
 * and environment variable presence.
 */
export function checkRequirements(requirements?: SkillRequirements): { available: boolean; reasons: string[] } {
  const reasons: string[] = [];

  if (!requirements) {
    return { available: true, reasons: [] };
  }

  // Check OS platform
  if (requirements.os && requirements.os.length > 0) {
    const currentPlatform = platform();
    if (!requirements.os.includes(currentPlatform)) {
      reasons.push(`Unsupported platform '${currentPlatform}' (requires: ${requirements.os.join(', ')})`);
    }
  }

  // Check binaries in PATH
  if (requirements.binaries) {
    const whichCmd = platform() === 'win32' ? 'where' : 'which';
    for (const binary of requirements.binaries) {
      try {
        execSync(`${whichCmd} ${binary}`, { stdio: 'ignore' });
      } catch {
        reasons.push(`Required binary '${binary}' not found in PATH`);
      }
    }
  }

  // Check environment variables
  if (requirements.env) {
    for (const envVar of requirements.env) {
      if (!process.env[envVar]) {
        reasons.push(`Required environment variable '${envVar}' is not set`);
      }
    }
  }

  return {
    available: reasons.length === 0,
    reasons,
  };
}

/**
 * Load a single skill from a directory.
 *
 * Reads manifest.json and SKILL.md. If manifest.json is missing, infers a
 * minimal manifest from the directory name and SKILL.md first line.
 * Returns null if SKILL.md doesn't exist or the manifest is unparseable.
 */
export function loadSkill(directory: string, tier: SkillTier): Skill | null {
  const absDir = resolve(directory);
  const manifestPath = join(absDir, 'manifest.json');
  const instructionsPath = join(absDir, 'SKILL.md');

  // SKILL.md is required — no instructions, no skill
  if (!existsSync(instructionsPath)) {
    return null;
  }

  const instructions = readFileSync(instructionsPath, 'utf-8');

  const scan = scanSkillDirectory(absDir);
  auditLogger?.skillsScan({
    skill_name: basename(absDir),
    tier,
    phase: 'load',
    ok: scan.ok,
    error_count: scan.issues.filter(i => i.severity === 'error').length,
    warn_count: scan.issues.filter(i => i.severity === 'warn').length,
    issue_codes: Array.from(new Set(scan.issues.map(i => i.code))),
  });
  const scanReasons = scan.issues.map((i) => `${i.code}: ${i.message}${i.path ? ` (${basename(i.path)})` : ''}`);

  const inferManifest = (): SkillManifest => {
    const name = basename(absDir);
    const firstLine = instructions.split('\n').find((line) => line.trim().length > 0) ?? name;
    const description = firstLine.replace(/^#+\s*/, '').trim();
    return {
      name,
      description,
      version: '0.0.0',
      tier,
    };
  };

  let manifest: SkillManifest = inferManifest();

  if (existsSync(manifestPath)) {
    // Parse manifest.json
    try {
      const raw = JSON.parse(readFileSync(manifestPath, 'utf-8'));

      if (raw && typeof raw === 'object' && raw.name && raw.description && raw.version) {
        manifest = {
          ...(raw as SkillManifest),
          tier, // Override tier from argument
        };
      }
    } catch (error) {
      // Scanner will capture this and mark the skill unavailable.
    }
  } else {
    manifest = inferManifest();
  }

  // Check system requirements
  const { available, reasons } = checkRequirements(manifest.requirements);

  const unavailableReasons = [...reasons];
  if (!scan.ok) {
    unavailableReasons.push(...scanReasons);
  }

  const skill: Skill = {
    manifest,
    instructions,
    directory: absDir,
    available: available && scan.ok,
  };

  if (!skill.available) {
    skill.unavailableReasons = unavailableReasons;
  }

  return skill;
}

/**
 * Discover all skills in a base directory.
 *
 * Lists subdirectories and attempts to load each as a skill.
 * Skips entries that aren't directories or fail to load.
 */
export function discoverSkills(baseDir: string, tier: SkillTier): Skill[] {
  const absBase = resolve(baseDir);

  if (!existsSync(absBase)) {
    return [];
  }

  const entries = readdirSync(absBase);
  const skills: Skill[] = [];

  for (const entry of entries) {
    const entryPath = join(absBase, entry);

    // Only process directories
    try {
      if (!statSync(entryPath).isDirectory()) {
        continue;
      }
    } catch {
      continue;
    }

    const skill = loadSkill(entryPath, tier);
    if (skill) {
      skills.push(skill);
    }
  }

  return skills;
}

/**
 * Load all skills from configured directories.
 *
 * Scans bundled, managed, and workspace directories (if provided and existing)
 * and returns a flat array of all discovered skills.
 */
export function loadAllSkills(config: {
  bundledDir?: string;
  managedDir?: string;
  workspaceDir?: string;
}): Skill[] {
  const skills: Skill[] = [];

  if (config.bundledDir) {
    skills.push(...discoverSkills(config.bundledDir, 'bundled'));
  }

  if (config.managedDir) {
    skills.push(...discoverSkills(config.managedDir, 'managed'));
  }

  if (config.workspaceDir) {
    skills.push(...discoverSkills(config.workspaceDir, 'workspace'));
  }

  return skills;
}
