/**
 * Vyper Guard — VS Code Extension
 *
 * Full-featured Vyper smart contract security analysis:
 *   • Real-time diagnostics on save (squiggly lines + Problems panel)
 *   • Quick-fix code actions (auto-remediation via 💡 lightbulb)
 *   • Workspace-wide scanning with progress
 *   • List available detectors
 *   • Version / environment info
 *   • Diff view for generated fixes
 *   • Status-bar security grade indicator
 *   • Explorer sidebar panel with scan results tree
 *
 * Architecture: all analysis is delegated to the `vyper-guard`
 * CLI as a subprocess (`--format json`).  Zero coupling with the
 * Python internals.
 */

import * as vscode from "vscode";
import * as cp from "child_process";
import * as path from "path";

/* ================================================================
   Types — mirror `vyper-guard analyze --format json` output
   ================================================================ */

interface Finding {
  detector_name: string;
  title: string;
  description: string;
  severity: string;
  confidence: string;
  line_number: number | null;
  end_line_number: number | null;
  source_snippet: string | null;
  fix_suggestion: string | null;
}

interface AnalysisReport {
  file_path: string;
  findings: Finding[];
  security_score: number;
  grade: { value: string; label: string };
  detectors_run: string[];
  vyper_version: string | null;
}

/* ================================================================
   Constants
   ================================================================ */

const SEVERITY_MAP: Record<string, vscode.DiagnosticSeverity> = {
  CRITICAL: vscode.DiagnosticSeverity.Error,
  HIGH: vscode.DiagnosticSeverity.Error,
  MEDIUM: vscode.DiagnosticSeverity.Warning,
  LOW: vscode.DiagnosticSeverity.Information,
  INFO: vscode.DiagnosticSeverity.Hint,
};

const SEVERITY_ICON: Record<string, string> = {
  CRITICAL: "🔴",
  HIGH: "🟠",
  MEDIUM: "🟡",
  LOW: "🔵",
  INFO: "⚪",
};

/* ================================================================
   Extension state
   ================================================================ */

let diagnosticCollection: vscode.DiagnosticCollection;
let outputChannel: vscode.OutputChannel;
let statusBarItem: vscode.StatusBarItem;

/** Per-file findings cache — powers Code Actions and detail view. */
const findingsCache = new Map<string, Finding[]>();
/** Per-file report cache — powers the results tree view. */
const reportsCache = new Map<string, AnalysisReport>();
/** Tree-view data provider — refreshed after every scan. */
let resultsProvider: ScanResultsProvider;

/* ================================================================
   Activation
   ================================================================ */

export function activate(ctx: vscode.ExtensionContext): void {
  outputChannel = vscode.window.createOutputChannel("Vyper Guard");
  diagnosticCollection =
    vscode.languages.createDiagnosticCollection("vyper-guard");

  statusBarItem = vscode.window.createStatusBarItem(
    vscode.StatusBarAlignment.Left,
    100,
  );
  statusBarItem.command = "vyperGuardian.analyzeFile";
  statusBarItem.tooltip = "Vyper Guard — click to scan";

  // Show / hide status bar based on active editor
  ctx.subscriptions.push(
    vscode.window.onDidChangeActiveTextEditor((ed) => {
      if (ed?.document.fileName.endsWith(".vy")) {
        statusBarItem.show();
      } else {
        statusBarItem.hide();
      }
    }),
  );

  // ── Register all commands ─────────────────────────────
  const cmds: Array<[string, (...a: never[]) => unknown]> = [
    ["vyperGuardian.analyzeFile", () => analyzeActiveFile(false)],
    ["vyperGuardian.analyzeAndFix", () => analyzeActiveFile(true)],
    ["vyperGuardian.analyzeWorkspace", analyzeWorkspace],
    ["vyperGuardian.listDetectors", listDetectors],
    ["vyperGuardian.showVersion", showVersion],
    ["vyperGuardian.clearDiagnostics", clearAllDiagnostics],
    ["vyperGuardian.showFindingDetail", showFindingDetail],
  ];
  for (const [id, handler] of cmds) {
    ctx.subscriptions.push(vscode.commands.registerCommand(id, handler));
  }

  // ── Code Action Provider (Quick Fix 💡) ───────────────
  ctx.subscriptions.push(
    vscode.languages.registerCodeActionsProvider(
      { language: "vyper", scheme: "file" },
      new VyperCodeActionProvider(),
      { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] },
    ),
  );

  // ── Tree-view: Scan Results (Explorer sidebar) ────────
  resultsProvider = new ScanResultsProvider();
  ctx.subscriptions.push(
    vscode.window.registerTreeDataProvider(
      "vyperGuardian.results",
      resultsProvider,
    ),
  );

  // ── Analyze on save ───────────────────────────────────
  ctx.subscriptions.push(
    vscode.workspace.onDidSaveTextDocument((doc) => {
      const cfg = vscode.workspace.getConfiguration("vyperGuardian");
      if (cfg.get<boolean>("analyzeOnSave") && doc.fileName.endsWith(".vy")) {
        runAnalysis(doc.uri.fsPath, false).then(() =>
          resultsProvider.refresh(),
        );
      }
    }),
  );

  // ── Tidy up on file close ─────────────────────────────
  ctx.subscriptions.push(
    vscode.workspace.onDidCloseTextDocument((doc) => {
      diagnosticCollection.delete(doc.uri);
      findingsCache.delete(doc.uri.fsPath);
      reportsCache.delete(doc.uri.fsPath);
      resultsProvider.refresh();
    }),
  );

  ctx.subscriptions.push(diagnosticCollection, outputChannel, statusBarItem);

  // Kick off initial scan if a .vy file is already open
  const active = vscode.window.activeTextEditor;
  if (active?.document.fileName.endsWith(".vy")) {
    statusBarItem.show();
    runAnalysis(active.document.fileName, false).then(() =>
      resultsProvider.refresh(),
    );
  }

  outputChannel.appendLine("Vyper Guard extension activated ✓");
}

export function deactivate(): void {
  /* subscriptions handle disposal */
}

/* ================================================================
   Analyze single file
   ================================================================ */

async function analyzeActiveFile(withFix: boolean): Promise<void> {
  const editor = vscode.window.activeTextEditor;
  if (!editor || !editor.document.fileName.endsWith(".vy")) {
    vscode.window.showWarningMessage(
      "Open a .vy file to run Vyper Guard.",
    );
    return;
  }
  await editor.document.save();
  await runAnalysis(editor.document.fileName, withFix);
  resultsProvider.refresh();
}

async function runAnalysis(
  filePath: string,
  withFix: boolean,
): Promise<void> {
  const cfg = vscode.workspace.getConfiguration("vyperGuardian");
  const exe = cfg.get<string>("executablePath") || "vyper-guard";
  const threshold = cfg.get<string>("severityThreshold") || "INFO";
  const dets = cfg.get<string>("enabledDetectors") || "all";

  const args = [
    "analyze",
    filePath,
    "--format",
    "json",
    "--severity-threshold",
    threshold,
  ];
  if (dets !== "all") {
    args.push("--detectors", dets);
  }
  if (withFix) {
    args.push("--fix");
  }

  const fileName = path.basename(filePath);
  statusBarItem.text = `$(loading~spin) Scanning ${fileName}…`;
  outputChannel.appendLine(`\n▸ ${exe} ${args.join(" ")}`);

  try {
    const report = await execCliJson(exe, args);

    // Cache for code actions + tree view
    findingsCache.set(filePath, report.findings);
    reportsCache.set(filePath, report);
    applyDiagnostics(filePath, report);

    const n = report.findings.length;
    const g = report.grade.value;
    statusBarItem.text =
      n === 0
        ? `$(shield) ${g} — clean`
        : `$(shield) ${g} — ${n} finding${n > 1 ? "s" : ""}`;

    // ── Handle --fix output ──────────────────────────
    if (withFix && n > 0) {
      const fixedPath = filePath.replace(/\.vy$/, ".fixed.vy");
      const choice = await vscode.window.showInformationMessage(
        `Vyper Guard: ${n} fix${n > 1 ? "es" : ""} generated → ${path.basename(fixedPath)}`,
        "Open Fixed File",
        "Diff with Original",
        "Dismiss",
      );
      if (choice === "Open Fixed File") {
        const doc = await vscode.workspace.openTextDocument(fixedPath);
        await vscode.window.showTextDocument(doc, { preview: true });
      } else if (choice === "Diff with Original") {
        await vscode.commands.executeCommand(
          "vscode.diff",
          vscode.Uri.file(filePath),
          vscode.Uri.file(fixedPath),
          `${fileName} ↔ ${path.basename(fixedPath)}`,
        );
      }
    }
  } catch (err: unknown) {
    const msg = err instanceof Error ? err.message : String(err);
    statusBarItem.text = "$(shield) Error";
    outputChannel.appendLine(`✗ ${msg}`);
    vscode.window.showErrorMessage(`Vyper Guard: ${msg}`);
  }
}

/* ================================================================
   List detectors
   ================================================================ */

async function listDetectors(): Promise<void> {
  const exe = getExecutable();
  try {
    const text = await execCliRaw(exe, ["detectors"]);
    outputChannel.clear();
    outputChannel.appendLine(
      "═══ Vyper Guard — Available Detectors ═══\n",
    );
    outputChannel.appendLine(text);
    outputChannel.show(true);
  } catch (err: unknown) {
    showCliError(err);
  }
}

/* ================================================================
   Show version
   ================================================================ */

async function showVersion(): Promise<void> {
  const exe = getExecutable();
  try {
    const text = await execCliRaw(exe, ["version"]);
    outputChannel.clear();
    outputChannel.appendLine("═══ Vyper Guard — Environment ═══\n");
    outputChannel.appendLine(text);
    outputChannel.show(true);

    const firstLine =
      text
        .split("\n")
        .find(
          (l) =>
            l.toLowerCase().includes("version") ||
            l.toLowerCase().includes("vyper"),
        ) || text.split("\n")[0];
    if (firstLine.trim()) {
      vscode.window.showInformationMessage(
        `Vyper Guard: ${firstLine.trim()}`,
      );
    }
  } catch (err: unknown) {
    showCliError(err);
  }
}

/* ================================================================
   Clear diagnostics
   ================================================================ */

function clearAllDiagnostics(): void {
  diagnosticCollection.clear();
  findingsCache.clear();
  reportsCache.clear();
  statusBarItem.text = "$(shield) Vyper Guard";
  vscode.window.showInformationMessage(
    "Vyper Guard: All diagnostics cleared.",
  );
  resultsProvider.refresh();
}

/* ================================================================
   Show finding detail (invoked by code-action)
   ================================================================ */

function showFindingDetail(
  ...args: unknown[]
): void {
  const finding = args[0] as
    | Finding
    | { title: string; description: string }
    | undefined;
  if (!finding) {
    return;
  }

  outputChannel.clear();
  outputChannel.appendLine("═══ Finding Detail ═══\n");

  if ("detector_name" in finding) {
    const f = finding as Finding;
    outputChannel.appendLine(`Detector : ${f.detector_name}`);
    outputChannel.appendLine(
      `Severity : ${SEVERITY_ICON[f.severity] || "•"} ${f.severity}`,
    );
    outputChannel.appendLine(`Title    : ${f.title}`);
    outputChannel.appendLine(`Line     : ${f.line_number ?? "N/A"}`);
    outputChannel.appendLine(`\nDescription:\n  ${f.description}`);
    if (f.fix_suggestion) {
      outputChannel.appendLine(`\n💡 Suggestion:\n  ${f.fix_suggestion}`);
    }
    if (f.source_snippet) {
      outputChannel.appendLine(`\nSource:\n${f.source_snippet}`);
    }
  } else {
    outputChannel.appendLine(finding.title);
    outputChannel.appendLine(finding.description);
  }

  outputChannel.show(true);
}

/* ================================================================
   Workspace scan
   ================================================================ */

async function analyzeWorkspace(): Promise<void> {
  const files = await vscode.workspace.findFiles(
    "**/*.vy",
    "**/node_modules/**",
  );
  if (files.length === 0) {
    vscode.window.showInformationMessage("No .vy files found in workspace.");
    return;
  }

  let totalFindings = 0;
  await vscode.window.withProgress(
    {
      location: vscode.ProgressLocation.Notification,
      title: "Vyper Guard: Scanning workspace…",
      cancellable: true,
    },
    async (progress, token) => {
      for (let i = 0; i < files.length; i++) {
        if (token.isCancellationRequested) {
          break;
        }
        progress.report({
          message: `${path.basename(files[i].fsPath)} (${i + 1}/${files.length})`,
          increment: 100 / files.length,
        });
        await runAnalysis(files[i].fsPath, false);
        totalFindings += (
          findingsCache.get(files[i].fsPath) || []
        ).length;
      }
    },
  );

  resultsProvider.refresh();
  vscode.window.showInformationMessage(
    `Vyper Guard: Scanned ${files.length} file${files.length > 1 ? "s" : ""} — ` +
      `${totalFindings} finding${totalFindings !== 1 ? "s" : ""} total.`,
  );
}

/* ================================================================
   CLI execution helpers
   ================================================================ */

function getExecutable(): string {
  return (
    vscode.workspace
      .getConfiguration("vyperGuardian")
      .get<string>("executablePath") || "vyper-guard"
  );
}

function showCliError(err: unknown): void {
  const msg = err instanceof Error ? err.message : String(err);
  vscode.window.showErrorMessage(`Vyper Guard: ${msg}`);
}

/** Run CLI → parse stdout as JSON AnalysisReport. */
function execCliJson(
  exe: string,
  args: string[],
): Promise<AnalysisReport> {
  return new Promise((resolve, reject) => {
    const proc = cp.execFile(
      exe,
      args,
      {
        maxBuffer: 10 * 1024 * 1024,
        timeout: 60_000,
        env: { ...process.env, NO_COLOR: "1" },
      },
      (_err, stdout, stderr) => {
        if (stderr) {
          outputChannel.appendLine(stderr.trim());
        }
        if (!stdout.trim()) {
          reject(
            new Error(
              _err
                ? `CLI failed (exit ${_err.code}): ${stderr || _err.message}`
                : "CLI returned empty output",
            ),
          );
          return;
        }
        try {
          resolve(JSON.parse(stdout) as AnalysisReport);
        } catch {
          reject(
            new Error(
              `Bad JSON from CLI: ${stdout.substring(0, 200)}`,
            ),
          );
        }
      },
    );
    proc.on("error", (e) =>
      reject(
        (e as NodeJS.ErrnoException).code === "ENOENT"
          ? new Error(
              `'${exe}' not found. Install it with: pip install vyper-guard`,
            )
          : e,
      ),
    );
  });
}

/** Run CLI → return raw stdout text (for detectors / version). */
function execCliRaw(
  exe: string,
  args: string[],
): Promise<string> {
  return new Promise((resolve, reject) => {
    cp.execFile(
      exe,
      args,
      {
        maxBuffer: 5 * 1024 * 1024,
        timeout: 30_000,
        env: { ...process.env, NO_COLOR: "1" },
      },
      (err, stdout, stderr) => {
        if (err && !stdout.trim()) {
          reject(new Error(`CLI failed: ${stderr || err.message}`));
          return;
        }
        resolve(stdout);
      },
    ).on("error", (e) =>
      reject(
        (e as NodeJS.ErrnoException).code === "ENOENT"
          ? new Error(
              `'${exe}' not found. Install it with: pip install vyper-guard`,
            )
          : e,
      ),
    );
  });
}

/* ================================================================
   Diagnostics
   ================================================================ */

function applyDiagnostics(
  filePath: string,
  report: AnalysisReport,
): void {
  const uri = vscode.Uri.file(filePath);
  const diagnostics: vscode.Diagnostic[] = [];

  for (const f of report.findings) {
    const line = Math.max(0, (f.line_number ?? 1) - 1);
    const range = new vscode.Range(line, 0, line, 1000);
    const sev =
      SEVERITY_MAP[f.severity] ?? vscode.DiagnosticSeverity.Warning;

    const diag = new vscode.Diagnostic(
      range,
      `[${f.detector_name}] ${f.title}`,
      sev,
    );
    diag.source = "vyper-guard";
    diag.code = f.detector_name;

    if (f.fix_suggestion) {
      diag.message += `\n💡 ${f.fix_suggestion}`;
    }
    if (f.description) {
      diag.message += `\n${f.description}`;
    }

    diagnostics.push(diag);
  }

  diagnosticCollection.set(uri, diagnostics);

  outputChannel.appendLine(
    `  ${path.basename(filePath)}: ${report.security_score}/100 ` +
      `(${report.grade.value}) — ${diagnostics.length} finding${diagnostics.length !== 1 ? "s" : ""}`,
  );
}

/* ================================================================
   Code-action provider  (Quick Fix 💡 lightbulb)
   ================================================================ */

class VyperCodeActionProvider implements vscode.CodeActionProvider {
  provideCodeActions(
    doc: vscode.TextDocument,
    _range: vscode.Range,
    context: vscode.CodeActionContext,
  ): vscode.CodeAction[] {
    const actions: vscode.CodeAction[] = [];
    const cached = findingsCache.get(doc.uri.fsPath) || [];

    for (const diag of context.diagnostics) {
      if (diag.source !== "vyper-guard") {
        continue;
      }

      const match = cached.find(
        (f) =>
          f.detector_name === diag.code &&
          Math.max(0, (f.line_number ?? 1) - 1) === diag.range.start.line,
      );

      // ── Quick-fix: trigger auto-remediation ───────
      if (match?.fix_suggestion) {
        const fix = new vscode.CodeAction(
          `🛡️ Fix: ${match.fix_suggestion.substring(0, 80)}`,
          vscode.CodeActionKind.QuickFix,
        );
        fix.command = {
          command: "vyperGuardian.analyzeAndFix",
          title: "Auto-Fix with Vyper Guard",
        };
        fix.diagnostics = [diag];
        fix.isPreferred = true;
        actions.push(fix);
      }

      // ── Detail action ─────────────────────────────
      const detail = new vscode.CodeAction(
        `ℹ️ Details: ${match?.detector_name ?? diag.code}`,
        vscode.CodeActionKind.QuickFix,
      );
      detail.command = {
        command: "vyperGuardian.showFindingDetail",
        title: "Show Finding Detail",
        arguments: [
          match ?? {
            title: String(diag.code),
            description: diag.message,
          },
        ],
      };
      actions.push(detail);
    }

    return actions;
  }
}

/* ================================================================
   Scan Results Tree View  (Explorer sidebar panel)
   ================================================================ */

type TreeItem = FileResultItem | FindingItem;

class ScanResultsProvider implements vscode.TreeDataProvider<TreeItem> {
  private _onDidChange = new vscode.EventEmitter<
    TreeItem | undefined | void
  >();
  readonly onDidChangeTreeData = this._onDidChange.event;

  refresh(): void {
    this._onDidChange.fire();
  }

  getTreeItem(el: TreeItem): vscode.TreeItem {
    return el;
  }

  getChildren(el?: TreeItem): TreeItem[] {
    // Root level → one item per scanned file
    if (!el) {
      const items: FileResultItem[] = [];
      for (const [fp, report] of reportsCache) {
        items.push(new FileResultItem(fp, report));
      }
      return items.sort((a, b) =>
        (a.label as string).localeCompare(b.label as string),
      );
    }
    // Child level → findings under a file
    if (el instanceof FileResultItem) {
      return el.report.findings.map(
        (f) => new FindingItem(el.filePath, f),
      );
    }
    return [];
  }
}

class FileResultItem extends vscode.TreeItem {
  constructor(
    public readonly filePath: string,
    public readonly report: AnalysisReport,
  ) {
    const n = report.findings.length;
    const label = path.basename(filePath);
    super(
      label,
      n > 0
        ? vscode.TreeItemCollapsibleState.Expanded
        : vscode.TreeItemCollapsibleState.None,
    );

    this.description = `${report.security_score}/100 ${report.grade.value} — ${n} finding${n !== 1 ? "s" : ""}`;
    this.tooltip = filePath;
    this.iconPath =
      n === 0
        ? new vscode.ThemeIcon(
            "pass",
            new vscode.ThemeColor("testing.iconPassed"),
          )
        : new vscode.ThemeIcon(
            "warning",
            new vscode.ThemeColor("testing.iconFailed"),
          );
    this.contextValue = "fileResult";

    this.command = {
      command: "vscode.open",
      title: "Open File",
      arguments: [vscode.Uri.file(filePath)],
    };
  }
}

class FindingItem extends vscode.TreeItem {
  constructor(filePath: string, finding: Finding) {
    super(finding.title, vscode.TreeItemCollapsibleState.None);

    const icon = SEVERITY_ICON[finding.severity] || "•";
    this.description = `${icon} ${finding.severity} — ${finding.detector_name}`;
    this.tooltip = finding.description;
    this.iconPath = FindingItem.severityIcon(finding.severity);
    this.contextValue = "finding";

    if (finding.line_number) {
      const line = Math.max(0, finding.line_number - 1);
      this.command = {
        command: "vscode.open",
        title: "Go to Line",
        arguments: [
          vscode.Uri.file(filePath),
          { selection: new vscode.Range(line, 0, line, 0) },
        ],
      };
    }
  }

  private static severityIcon(severity: string): vscode.ThemeIcon {
    switch (severity) {
      case "CRITICAL":
      case "HIGH":
        return new vscode.ThemeIcon(
          "error",
          new vscode.ThemeColor("errorForeground"),
        );
      case "MEDIUM":
        return new vscode.ThemeIcon(
          "warning",
          new vscode.ThemeColor("editorWarning.foreground"),
        );
      case "LOW":
        return new vscode.ThemeIcon(
          "info",
          new vscode.ThemeColor("editorInfo.foreground"),
        );
      default:
        return new vscode.ThemeIcon("circle-outline");
    }
  }
}
