import { existsSync } from "node:fs";
import { createRequire } from "node:module";
import { join } from "node:path";
import type {
  LspServerKind,
  LspToolRuntimeStatusEntry,
  LspToolSource,
  Workspace,
} from "@coder-studio/core";
import {
  type CommandAvailabilityCheck,
  type CommandCheckDeps,
  checkCommandAvailable,
} from "../provider-runtime/command-check.js";
import { type CommandRunner, runCommandAsString } from "../provider-runtime/command-runner.js";
import {
  getLspCommandOverridePrefix,
  getLspToolDefinition,
  getManagedPrerequisites,
  type LspToolDefinition,
  resolveManagedPythonCommand,
} from "./definitions.js";
import { type FileManifestStore, type ManagedLspToolManifest } from "./manifest-store.js";

const require = createRequire(import.meta.url);

export interface ResolvedLspToolCommand {
  kind: "ready";
  serverKind: LspServerKind;
  displayName: string;
  source: LspToolSource;
  command: string;
  args: string[];
}

export interface MissingLspToolCommand {
  kind: "tool_missing";
  serverKind: LspServerKind;
  displayName: string;
  errorCode: "lsp_tool_missing" | "lsp_prerequisite_missing";
  message: string;
  autoInstallSupported: boolean;
  installReadiness: LspToolRuntimeStatusEntry["installReadiness"];
  missingCommands: string[];
  missingPrerequisites: string[];
}

export type LspToolResolveResult = ResolvedLspToolCommand | MissingLspToolCommand;

export interface LspToolManagerDeps extends CommandCheckDeps {
  manifestStore: FileManifestStore;
  commandExists?: CommandAvailabilityCheck;
  platform?: NodeJS.Platform;
  resolveBundledCommand?: (serverKind: LspServerKind) => { command: string; args: string[] } | null;
}

export class LspToolManager {
  constructor(private readonly deps: LspToolManagerDeps) {}

  async resolve(input: {
    workspace: Workspace;
    serverKind: LspServerKind;
    env?: NodeJS.ProcessEnv;
  }): Promise<LspToolResolveResult> {
    const env = input.env ?? process.env;
    const commandExists =
      this.deps.commandExists ?? ((command: string) => checkCommandAvailable(command, this.deps));
    const definition = getLspToolDefinition(input.serverKind);

    const override = this.resolveOverride(input.serverKind, env);
    if (override) {
      return {
        kind: "ready",
        serverKind: input.serverKind,
        displayName: definition.displayName,
        source: "override",
        command: override.command,
        args: override.args,
      };
    }

    const managed = this.resolveManaged(definition);
    if (managed) {
      return {
        kind: "ready",
        serverKind: input.serverKind,
        displayName: definition.displayName,
        source: "managed",
        command: managed.executablePath,
        args: definition.defaultArgs,
      };
    }

    const bundled = this.resolveBundled(input.serverKind, definition);
    if (bundled) {
      return {
        kind: "ready",
        serverKind: input.serverKind,
        displayName: definition.displayName,
        source: "bundled",
        command: bundled.command,
        args: bundled.args,
      };
    }

    if (await this.isSystemCommandUsable(definition.defaultCommand, commandExists)) {
      return {
        kind: "ready",
        serverKind: input.serverKind,
        displayName: definition.displayName,
        source: "system",
        command: definition.defaultCommand,
        args: definition.defaultArgs,
      };
    }

    return this.buildMissingResult(input.workspace, definition, commandExists);
  }

  async runtimeStatus(input: {
    workspace: Workspace;
    serverKind: LspServerKind;
    env?: NodeJS.ProcessEnv;
  }): Promise<LspToolRuntimeStatusEntry> {
    const result = await this.resolve(input);
    if (result.kind === "ready") {
      return {
        serverKind: result.serverKind,
        displayName: result.displayName,
        available: true,
        source: result.source,
        autoInstallSupported: false,
        installReadiness: "ready",
        missingCommands: [],
        missingPrerequisites: [],
      };
    }

    return {
      serverKind: result.serverKind,
      displayName: result.displayName,
      available: false,
      autoInstallSupported: result.autoInstallSupported,
      installReadiness: result.installReadiness,
      missingCommands: result.missingCommands,
      missingPrerequisites: result.missingPrerequisites,
      message: result.message,
    };
  }

  /**
   * Decide whether a system-PATH command should be treated as a usable LSP
   * source. `commandExists` only checks PATH presence, but Windows is full of
   * zero-byte or proxy shims that satisfy that check yet refuse to actually
   * run:
   *
   *  - `%LOCALAPPDATA%\Microsoft\WindowsApps\python(3).exe` — Microsoft
   *    Store app execution aliases that redirect to the Store when the
   *    underlying app isn't installed.
   *  - `~/.cargo/bin/rust-analyzer.exe` when the `rust-analyzer` rustup
   *    component isn't installed — the rustup proxy prints
   *    "Unknown binary 'rust-analyzer.exe' in official toolchain" and exits.
   *
   * Accepting either of those as a "system" source leads to ambiguous LSP
   * timeouts the first time a hover lands. Probe with `--version` on
   * Windows so we fall through to the managed install path instead.
   */
  private async isSystemCommandUsable(
    command: string,
    commandExists: CommandAvailabilityCheck
  ): Promise<boolean> {
    if (!(await commandExists(command))) {
      return false;
    }
    const platform = this.deps.platform ?? process.platform;
    if (platform !== "win32") {
      return true;
    }
    const runCommand: CommandRunner = this.deps.runCommand ?? runCommandAsString;
    try {
      const result = await runCommand(command, ["--version"], { windowsHide: true });
      return `${result.stdout}\n${result.stderr}`.trim().length > 0;
    } catch {
      return false;
    }
  }

  private resolveOverride(
    serverKind: LspServerKind,
    env: NodeJS.ProcessEnv
  ): { command: string; args: string[] } | null {
    const prefix = getLspCommandOverridePrefix(serverKind);
    const command = env[`${prefix}_COMMAND`];
    if (!command) {
      return null;
    }

    const argsJson = env[`${prefix}_ARGS_JSON`];
    const args = argsJson ? parseOverrideArgs(argsJson, `${prefix}_ARGS_JSON`) : [];
    return { command, args };
  }

  private resolveManaged(definition: LspToolDefinition): ManagedLspToolManifest | null {
    if (!definition.managed) {
      return null;
    }

    const manifest = this.deps.manifestStore.read(definition.serverKind);
    if (!manifest) {
      return null;
    }

    const platform = this.deps.platform ?? process.platform;
    if (
      manifest.serverKind !== definition.serverKind ||
      manifest.version !== definition.managed.version ||
      manifest.source !== "managed" ||
      manifest.platform !== platform ||
      typeof manifest.executablePath !== "string" ||
      manifest.executablePath.trim().length === 0
    ) {
      return null;
    }

    if (!existsSync(manifest.executablePath)) {
      return null;
    }

    return manifest;
  }

  private resolveBundled(
    serverKind: LspServerKind,
    definition: LspToolDefinition
  ): { command: string; args: string[] } | null {
    if (this.deps.resolveBundledCommand) {
      return this.deps.resolveBundledCommand(serverKind);
    }

    if (!definition.bundled) {
      return null;
    }

    try {
      const packageJsonPath = require.resolve(`${definition.bundled.packageName}/package.json`);
      const packageRoot = join(packageJsonPath, "..");
      const entryPath = join(packageRoot, definition.bundled.entry);
      if (!existsSync(entryPath)) {
        return null;
      }

      const command = definition.bundled.launchWithNode ? process.execPath : entryPath;
      const args = definition.bundled.launchWithNode
        ? [entryPath, ...definition.bundled.args]
        : definition.bundled.args;

      return {
        command,
        args,
      };
    } catch {
      return null;
    }
  }

  private async buildMissingResult(
    workspace: Workspace,
    definition: LspToolDefinition,
    commandExists: CommandAvailabilityCheck
  ): Promise<MissingLspToolCommand> {
    const missingCommands = [definition.defaultCommand];
    const missingPrerequisites: string[] = [];
    const managed = definition.managed;
    const autoInstallSupported = Boolean(managed) && workspace.targetRuntime === "native";
    const platform = this.deps.platform ?? process.platform;

    if (managed && workspace.targetRuntime === "native") {
      if (definition.serverKind === "python") {
        const pythonCommand = await resolveManagedPythonCommand(
          commandExists,
          platform,
          this.deps.runCommand
        );
        if (!pythonCommand) {
          missingPrerequisites.push(...getManagedPrerequisites("python", platform));
        }
      } else {
        for (const prerequisite of getManagedPrerequisites(definition.serverKind, platform)) {
          if (!(await commandExists(prerequisite))) {
            missingPrerequisites.push(prerequisite);
          }
        }
      }
    }

    const installReadiness =
      !managed || workspace.targetRuntime !== "native"
        ? "unsupported_platform"
        : missingPrerequisites.length > 0
          ? "missing_prerequisite"
          : "ready";

    return {
      kind: "tool_missing",
      serverKind: definition.serverKind,
      displayName: definition.displayName,
      errorCode: missingPrerequisites.length > 0 ? "lsp_prerequisite_missing" : "lsp_tool_missing",
      message:
        missingPrerequisites.length > 0
          ? `Missing prerequisites for ${definition.displayName}: ${missingPrerequisites.join(", ")}`
          : `${definition.displayName} is not installed`,
      autoInstallSupported,
      installReadiness,
      missingCommands,
      missingPrerequisites,
    };
  }
}

function parseOverrideArgs(raw: string, envVarName: string): string[] {
  try {
    const parsed = JSON.parse(raw) as unknown;
    if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) {
      throw new Error("override args must be a string array");
    }

    return parsed;
  } catch {
    throw new Error(`Invalid JSON in ${envVarName}`);
  }
}
