import { lstatSync, readdirSync, readFileSync, existsSync, statSync } from 'fs';
import { join, resolve } from 'path';

export type SkillScanSeverity = 'error' | 'warn';

export interface SkillScanIssue {
  severity: SkillScanSeverity;
  code: string;
  message: string;
  path?: string;
}

export interface SkillScanOptions {
  maxFileSizeBytes?: number;
  failOnWarnings?: boolean;
}

export interface SkillScanResult {
  ok: boolean;
  issues: SkillScanIssue[];
}

const DEFAULT_MAX_FILE_SIZE_BYTES = 1_000_000;

function isProbablyBinary(buf: Buffer): boolean {
  if (buf.includes(0)) {
    return true;
  }

  const sample = buf.subarray(0, Math.min(buf.length, 8192));
  let suspicious = 0;
  for (const b of sample) {
    // Allow common whitespace and printable ASCII.
    const isAllowed = b === 9 || b === 10 || b === 13 || (b >= 32 && b <= 126);
    if (!isAllowed) {
      suspicious++;
    }
  }
  // If more than 25% of sampled bytes are non-text, treat as binary.
  return sample.length > 0 && suspicious / sample.length > 0.25;
}

function walk(dir: string, onEntry: (absPath: string) => void): void {
  const entries = readdirSync(dir, { withFileTypes: true });
  for (const entry of entries) {
    const abs = join(dir, entry.name);
    onEntry(abs);
    if (entry.isDirectory()) {
      walk(abs, onEntry);
    }
  }
}

function scanPromptInjectionMarkers(text: string, filePath: string): SkillScanIssue[] {
  const patterns: Array<{ code: string; re: RegExp; message: string }> = [
    { code: 'prompt.ignore_previous', re: /\bignore\s+(?:all|any|previous)\s+instructions\b/i, message: 'Prompt-injection marker: ignore previous instructions' },
    { code: 'prompt.system_prompt', re: /\bsystem\s+prompt\b/i, message: 'Prompt-injection marker: mentions system prompt' },
    { code: 'prompt.exfiltrate', re: /\bexfiltrat(e|ion)\b/i, message: 'Prompt-injection marker: exfiltration language' },
    { code: 'prompt.send_secrets', re: /\bsend\s+secrets?\b/i, message: 'Prompt-injection marker: requests sending secrets' },
    { code: 'prompt.api_key', re: /\b(api\s*key|access\s*token|refresh\s*token)\b/i, message: 'Prompt-injection marker: requests API keys/tokens' },
  ];

  const issues: SkillScanIssue[] = [];
  for (const p of patterns) {
    if (p.re.test(text)) {
      issues.push({ severity: 'error', code: p.code, message: p.message, path: filePath });
    }
  }
  return issues;
}

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

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

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 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;
}

function safeJsonParse(raw: string): unknown {
  return JSON.parse(raw) as unknown;
}

/**
 * Scan a skill directory for common safety hazards.
 *
 * MVP rules:
 * - deny any symlinks
 * - deny files over max size
 * - deny binary blobs for SKILL.md / manifest.json
 * - manifest.json must parse as JSON (if present)
 * - lightweight prompt-injection marker scan for SKILL.md
 */
export function scanSkillDirectory(directory: string, opts: SkillScanOptions = {}): SkillScanResult {
  const issues: SkillScanIssue[] = [];
  const absDir = resolve(directory);
  const maxBytes = opts.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
  const failOnWarnings = Boolean(opts.failOnWarnings);

  // Deny symlinks + deny oversized files anywhere inside the skill directory.
  if (existsSync(absDir)) {
    walk(absDir, (absPath) => {
      let st;
      try {
        st = lstatSync(absPath);
      } catch {
        return;
      }

      if (st.isSymbolicLink()) {
        issues.push({
          severity: 'error',
          code: 'fs.symlink',
          message: 'Symlinks are not allowed inside skill directories',
          path: absPath,
        });
        return;
      }

      if (st.isFile() && st.size > maxBytes) {
        issues.push({
          severity: 'error',
          code: 'fs.oversize',
          message: `File exceeds max size (${st.size} > ${maxBytes} bytes)`,
          path: absPath,
        });
      }
    });
  }

  const skillMdPath = join(absDir, 'SKILL.md');
  if (existsSync(skillMdPath)) {
    try {
      const buf = readFileSync(skillMdPath);
      if (isProbablyBinary(buf)) {
        issues.push({ severity: 'error', code: 'content.binary', message: 'SKILL.md appears to be binary', path: skillMdPath });
      } else {
        const text = buf.toString('utf-8');
        issues.push(...scanPromptInjectionMarkers(text, skillMdPath));
      }
    } catch {
      // Ignore read errors here; loader will handle missing/invalid instructions separately.
    }
  }

  const manifestPath = join(absDir, 'manifest.json');
  if (existsSync(manifestPath)) {
    try {
      const buf = readFileSync(manifestPath);
      if (isProbablyBinary(buf)) {
        issues.push({ severity: 'error', code: 'content.binary', message: 'manifest.json appears to be binary', path: manifestPath });
      } else {
        const text = buf.toString('utf-8');
        const raw = safeJsonParse(text) as any;
        if (!raw || typeof raw !== 'object') {
          issues.push({ severity: 'error', code: 'manifest.missing_required_fields', message: 'manifest.json must be an object with required fields', path: manifestPath });
        } else {
          if (!raw.name || !raw.description || !raw.version) {
            issues.push({ severity: 'error', code: 'manifest.missing_required_fields', message: 'manifest.json missing required fields (name, description, version)', path: manifestPath });
          }
          if (!hasValidInstallers(raw)) {
            issues.push({ severity: 'error', code: 'manifest.invalid_installers', message: 'manifest.json has invalid installers specification', path: manifestPath });
          }
          if (!hasValidPermissions(raw)) {
            issues.push({ severity: 'error', code: 'manifest.invalid_permissions', message: 'manifest.json has invalid permissions specification', path: manifestPath });
          }

          // Soft guidance: permissions should exist for routable skills.
          if (!raw.permissions) {
            issues.push({ severity: 'warn', code: 'manifest.permissions_missing', message: 'manifest.json has no permissions block (skill will not be routable)' , path: manifestPath });
          }
        }
      }
    } catch {
      issues.push({ severity: 'error', code: 'manifest.invalid_json', message: 'manifest.json must be valid JSON', path: manifestPath });
    }
  }

  // Best-effort check for extremely large SKILL.md / manifest.json too (covers pre-walk calls).
  for (const p of [skillMdPath, manifestPath]) {
    if (!existsSync(p)) {
      continue;
    }
    try {
      const st = statSync(p);
      if (st.isFile() && st.size > maxBytes) {
        issues.push({ severity: 'error', code: 'fs.oversize', message: `File exceeds max size (${st.size} > ${maxBytes} bytes)`, path: p });
      }
    } catch {
      // ignore
    }
  }

  const hasError = issues.some(i => i.severity === 'error');
  const hasWarn = issues.some(i => i.severity === 'warn');
  const ok = !hasError && (!failOnWarnings || !hasWarn);
  return { ok, issues };
}
