import { basename, join, relative } from "node:path";

import { z } from "zod/mini";

import { FeatureProcessor } from "../../types/feature-processor.js";
import { RulesyncFile } from "../../types/rulesync-file.js";
import { ToolFile } from "../../types/tool-file.js";
import type { ToolTarget } from "../../types/tool-targets.js";
import { formatError } from "../../utils/error.js";
import { checkPathTraversal, findFilesByGlobs } from "../../utils/file.js";
import type { Logger } from "../../utils/logger.js";
import { AgentsmdCommand } from "./agentsmd-command.js";
import { AntigravityCommand } from "./antigravity-command.js";
import { ClaudecodeCommand } from "./claudecode-command.js";
import { ClineCommand } from "./cline-command.js";
import { CodexcliCommand } from "./codexcli-command.js";
import { CopilotCommand } from "./copilot-command.js";
import { CursorCommand } from "./cursor-command.js";
import { FactorydroidCommand } from "./factorydroid-command.js";
import { GeminiCliCommand } from "./geminicli-command.js";
import { JunieCommand } from "./junie-command.js";
import { KiloCommand } from "./kilo-command.js";
import { KiroCommand } from "./kiro-command.js";
import { OpenCodeCommand } from "./opencode-command.js";
import { PiCommand } from "./pi-command.js";
import { RooCommand } from "./roo-command.js";
import { RulesyncCommand } from "./rulesync-command.js";
import { TaktCommand } from "./takt-command.js";
import {
  ToolCommand,
  ToolCommandForDeletionParams,
  ToolCommandFromFileParams,
  ToolCommandFromRulesyncCommandParams,
  ToolCommandSettablePaths,
} from "./tool-command.js";

/**
 * Factory entry for each tool command class.
 * Stores the class reference and metadata for a tool.
 */
type ToolCommandFactory = {
  class: {
    isTargetedByRulesyncCommand(rulesyncCommand: RulesyncCommand): boolean;
    fromRulesyncCommand(params: ToolCommandFromRulesyncCommandParams): ToolCommand;
    fromFile(params: ToolCommandFromFileParams): Promise<ToolCommand>;
    forDeletion(params: ToolCommandForDeletionParams): ToolCommand;
    getSettablePaths(options?: { global?: boolean }): ToolCommandSettablePaths;
  };
  meta: {
    /** File extension for the command file */
    extension: "md" | "toml" | "prompt.md";
    /** Whether the tool supports project-level commands */
    supportsProject: boolean;
    /** Whether the tool supports global (user-level) commands */
    supportsGlobal: boolean;
    /** Whether the command is simulated (embedded in rules) */
    isSimulated: boolean;
    /** Whether the tool supports subdirectory paths in commands */
    supportsSubdirectory: boolean;
  };
};

/**
 * Supported tool targets for CommandsProcessor.
 * Using a tuple to preserve order for consistent iteration.
 */
const commandsProcessorToolTargetTuple = [
  "agentsmd",
  "antigravity",
  "claudecode",
  "claudecode-legacy",
  "cline",
  "codexcli",
  "copilot",
  "cursor",
  "factorydroid",
  "geminicli",
  "junie",
  "kilo",
  "kiro",
  "opencode",
  "pi",
  "roo",
  "takt",
] as const;

export type CommandsProcessorToolTarget = (typeof commandsProcessorToolTargetTuple)[number];

// Schema for runtime validation
export const CommandsProcessorToolTargetSchema = z.enum(commandsProcessorToolTargetTuple);

/**
 * Factory Map mapping tool targets to their command factories.
 * Using Map to preserve insertion order for consistent iteration.
 */
const toolCommandFactories = new Map<CommandsProcessorToolTarget, ToolCommandFactory>([
  [
    "agentsmd",
    {
      class: AgentsmdCommand,
      meta: {
        extension: "md",
        supportsProject: true,
        supportsGlobal: false,
        isSimulated: true,
        supportsSubdirectory: false,
      },
    },
  ],
  [
    "antigravity",
    {
      class: AntigravityCommand,
      meta: {
        extension: "md",
        supportsProject: true,
        supportsGlobal: false,
        isSimulated: false,
        supportsSubdirectory: false,
      },
    },
  ],
  [
    "claudecode",
    {
      class: ClaudecodeCommand,
      meta: {
        extension: "md",
        supportsProject: true,
        supportsGlobal: true,
        isSimulated: false,
        supportsSubdirectory: true,
      },
    },
  ],
  [
    "claudecode-legacy",
    {
      class: ClaudecodeCommand,
      meta: {
        extension: "md",
        supportsProject: true,
        supportsGlobal: true,
        isSimulated: false,
        supportsSubdirectory: true,
      },
    },
  ],
  [
    "cline",
    {
      class: ClineCommand,
      meta: {
        extension: "md",
        supportsProject: true,
        supportsGlobal: true,
        isSimulated: false,
        supportsSubdirectory: false,
      },
    },
  ],
  [
    "codexcli",
    {
      class: CodexcliCommand,
      meta: {
        extension: "md",
        supportsProject: false,
        supportsGlobal: true,
        isSimulated: false,
        supportsSubdirectory: false,
      },
    },
  ],
  [
    "copilot",
    {
      class: CopilotCommand,
      meta: {
        extension: "prompt.md",
        supportsProject: true,
        supportsGlobal: false,
        isSimulated: false,
        supportsSubdirectory: false,
      },
    },
  ],
  [
    "cursor",
    {
      class: CursorCommand,
      meta: {
        extension: "md",
        supportsProject: true,
        supportsGlobal: true,
        isSimulated: false,
        supportsSubdirectory: false,
      },
    },
  ],
  [
    "factorydroid",
    {
      class: FactorydroidCommand,
      meta: {
        extension: "md",
        supportsProject: true,
        supportsGlobal: false,
        isSimulated: true,
        supportsSubdirectory: false,
      },
    },
  ],
  [
    "geminicli",
    {
      class: GeminiCliCommand,
      meta: {
        extension: "toml",
        supportsProject: true,
        supportsGlobal: true,
        isSimulated: false,
        supportsSubdirectory: true,
      },
    },
  ],
  [
    "junie",
    {
      class: JunieCommand,
      meta: {
        extension: "md",
        supportsProject: true,
        supportsGlobal: true,
        isSimulated: false,
        supportsSubdirectory: false,
      },
    },
  ],
  [
    "kilo",
    {
      class: KiloCommand,
      meta: {
        extension: "md",
        supportsProject: true,
        supportsGlobal: true,
        isSimulated: false,
        supportsSubdirectory: false,
      },
    },
  ],
  [
    "kiro",
    {
      class: KiroCommand,
      meta: {
        extension: "md",
        supportsProject: true,
        supportsGlobal: false,
        isSimulated: false,
        supportsSubdirectory: false,
      },
    },
  ],
  [
    "opencode",
    {
      class: OpenCodeCommand,
      meta: {
        extension: "md",
        supportsProject: true,
        supportsGlobal: true,
        isSimulated: false,
        supportsSubdirectory: true,
      },
    },
  ],
  [
    "pi",
    {
      class: PiCommand,
      meta: {
        extension: "md",
        supportsProject: true,
        supportsGlobal: true,
        isSimulated: false,
        supportsSubdirectory: false,
      },
    },
  ],
  [
    "roo",
    {
      class: RooCommand,
      meta: {
        extension: "md",
        supportsProject: true,
        supportsGlobal: false,
        isSimulated: false,
        supportsSubdirectory: true,
      },
    },
  ],
  [
    "takt",
    {
      class: TaktCommand,
      meta: {
        extension: "md",
        supportsProject: true,
        supportsGlobal: true,
        isSimulated: false,
        supportsSubdirectory: false,
      },
    },
  ],
]);

/**
 * Factory retrieval function type for dependency injection.
 * Allows injecting custom factory implementations for testing purposes.
 */
type GetFactory = (target: CommandsProcessorToolTarget) => ToolCommandFactory;

const defaultGetFactory: GetFactory = (target) => {
  const factory = toolCommandFactories.get(target);
  if (!factory) {
    throw new Error(`Unsupported tool target: ${target}`);
  }
  return factory;
};

// Derive tool target arrays from factory metadata
const allToolTargetKeys = [...toolCommandFactories.keys()];

const commandsProcessorToolTargets: ToolTarget[] = allToolTargetKeys.filter((target) => {
  const factory = toolCommandFactories.get(target);
  return factory?.meta.supportsProject ?? false;
});

const commandsProcessorToolTargetsSimulated: ToolTarget[] = allToolTargetKeys.filter((target) => {
  const factory = toolCommandFactories.get(target);
  return factory?.meta.isSimulated ?? false;
});

export const commandsProcessorToolTargetsGlobal: ToolTarget[] = allToolTargetKeys.filter(
  (target) => {
    const factory = toolCommandFactories.get(target);
    return factory?.meta.supportsGlobal ?? false;
  },
);

export class CommandsProcessor extends FeatureProcessor {
  private readonly toolTarget: CommandsProcessorToolTarget;
  private readonly global: boolean;
  private readonly getFactory: GetFactory;

  constructor({
    outputRoot = process.cwd(),
    inputRoot = process.cwd(),
    toolTarget,
    global = false,
    getFactory = defaultGetFactory,
    dryRun = false,
    logger,
  }: {
    outputRoot?: string;
    inputRoot?: string;
    toolTarget: ToolTarget;
    global?: boolean;
    getFactory?: GetFactory;
    dryRun?: boolean;
    logger: Logger;
  }) {
    super({ outputRoot, inputRoot, dryRun, logger });
    const result = CommandsProcessorToolTargetSchema.safeParse(toolTarget);
    if (!result.success) {
      throw new Error(
        `Invalid tool target for CommandsProcessor: ${toolTarget}. ${formatError(result.error)}`,
      );
    }
    this.toolTarget = result.data;
    this.global = global;
    this.getFactory = getFactory;
  }

  async convertRulesyncFilesToToolFiles(rulesyncFiles: RulesyncFile[]): Promise<ToolFile[]> {
    const rulesyncCommands = rulesyncFiles.filter(
      (file): file is RulesyncCommand => file instanceof RulesyncCommand,
    );

    const factory = this.getFactory(this.toolTarget);
    const flattenedPathOrigins = new Map<string, string>();

    const toolCommands = rulesyncCommands
      .map((rulesyncCommand) => {
        if (!factory.class.isTargetedByRulesyncCommand(rulesyncCommand)) {
          return null;
        }
        const originalRelativePath = rulesyncCommand.getRelativeFilePath();
        const commandToConvert = factory.meta.supportsSubdirectory
          ? rulesyncCommand
          : this.flattenRelativeFilePath(rulesyncCommand);
        if (!factory.meta.supportsSubdirectory) {
          const flattenedPath = commandToConvert.getRelativeFilePath();
          const firstOrigin = flattenedPathOrigins.get(flattenedPath);
          if (firstOrigin && firstOrigin !== originalRelativePath) {
            this.logger.warn(
              `Command path collision detected while flattening for ${this.toolTarget}: "${firstOrigin}" and "${originalRelativePath}" both map to "${flattenedPath}". Only the last processed command will be used.`,
            );
          } else if (!firstOrigin) {
            flattenedPathOrigins.set(flattenedPath, originalRelativePath);
          }
        }
        return factory.class.fromRulesyncCommand({
          outputRoot: this.outputRoot,
          rulesyncCommand: commandToConvert,
          global: this.global,
        });
      })
      .filter((command): command is ToolCommand => command !== null);

    return toolCommands;
  }

  async convertToolFilesToRulesyncFiles(toolFiles: ToolFile[]): Promise<RulesyncFile[]> {
    const toolCommands = toolFiles.filter(
      (file): file is ToolCommand => file instanceof ToolCommand,
    );

    const rulesyncCommands = toolCommands.map((toolCommand) => {
      return toolCommand.toRulesyncCommand();
    });

    return rulesyncCommands;
  }

  private flattenRelativeFilePath(rulesyncCommand: RulesyncCommand): RulesyncCommand {
    const flatPath = basename(rulesyncCommand.getRelativeFilePath());
    if (flatPath === rulesyncCommand.getRelativeFilePath()) return rulesyncCommand;
    return rulesyncCommand.withRelativeFilePath(flatPath);
  }

  private safeRelativePath(basePath: string, fullPath: string): string {
    const rel = relative(basePath, fullPath);
    checkPathTraversal({ relativePath: rel, intendedRootDir: basePath });
    return rel;
  }

  /**
   * Implementation of abstract method from FeatureProcessor
   * Load and parse rulesync command files from .rulesync/commands/ directory
   */
  async loadRulesyncFiles(): Promise<RulesyncFile[]> {
    const basePath = join(this.inputRoot, RulesyncCommand.getSettablePaths().relativeDirPath);
    const rulesyncCommandPaths = await findFilesByGlobs(join(basePath, "**", "*.md"));

    const rulesyncCommands = await Promise.all(
      rulesyncCommandPaths.map((path) =>
        RulesyncCommand.fromFile({
          outputRoot: this.inputRoot,
          relativeFilePath: this.safeRelativePath(basePath, path),
        }),
      ),
    );

    this.logger.debug(`Successfully loaded ${rulesyncCommands.length} rulesync commands`);
    return rulesyncCommands;
  }

  /**
   * Implementation of abstract method from FeatureProcessor
   * Load tool-specific command configurations and parse them into ToolCommand instances
   */
  async loadToolFiles({
    forDeletion = false,
  }: {
    forDeletion?: boolean;
  } = {}): Promise<ToolFile[]> {
    const factory = this.getFactory(this.toolTarget);
    const paths = factory.class.getSettablePaths({ global: this.global });

    const outputRootFull = join(this.outputRoot, paths.relativeDirPath);
    const globPattern = factory.meta.supportsSubdirectory
      ? join(outputRootFull, "**", `*.${factory.meta.extension}`)
      : join(outputRootFull, `*.${factory.meta.extension}`);
    const commandFilePaths = await findFilesByGlobs(globPattern);

    if (forDeletion) {
      const toolCommands = commandFilePaths
        .map((path) =>
          factory.class.forDeletion({
            outputRoot: this.outputRoot,
            relativeDirPath: paths.relativeDirPath,
            relativeFilePath: this.safeRelativePath(outputRootFull, path),
            global: this.global,
          }),
        )
        .filter((cmd) => cmd.isDeletable());

      this.logger.debug(
        `Successfully loaded ${toolCommands.length} ${paths.relativeDirPath} commands`,
      );
      return toolCommands;
    }

    const toolCommands = await Promise.all(
      commandFilePaths.map((path) =>
        factory.class.fromFile({
          outputRoot: this.outputRoot,
          relativeFilePath: this.safeRelativePath(outputRootFull, path),
          global: this.global,
        }),
      ),
    );

    this.logger.debug(
      `Successfully loaded ${toolCommands.length} ${paths.relativeDirPath} commands`,
    );
    return toolCommands;
  }

  /**
   * Implementation of abstract method from FeatureProcessor
   * Return the tool targets that this processor supports
   */
  static getToolTargets({
    global = false,
    includeSimulated = false,
  }: {
    global?: boolean;
    includeSimulated?: boolean;
  } = {}): ToolTarget[] {
    if (global) {
      return [...commandsProcessorToolTargetsGlobal];
    }
    if (!includeSimulated) {
      return commandsProcessorToolTargets.filter(
        (target) => !commandsProcessorToolTargetsSimulated.includes(target),
      );
    }
    return [...commandsProcessorToolTargets];
  }

  static getToolTargetsSimulated(): ToolTarget[] {
    return [...commandsProcessorToolTargetsSimulated];
  }

  /**
   * Get the factory for a specific tool target.
   * This is a static version of the internal getFactory for external use.
   * @param target - The tool target. Must be a valid CommandsProcessorToolTarget.
   * @returns The factory for the target, or undefined if not found.
   */
  static getFactory(target: ToolTarget): ToolCommandFactory | undefined {
    // Validate that target is supported
    const result = CommandsProcessorToolTargetSchema.safeParse(target);
    if (!result.success) {
      return undefined;
    }
    return toolCommandFactories.get(result.data);
  }
}
