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

import { z } from "zod/mini";

import { RULESYNC_RULES_RELATIVE_DIR_PATH } from "../constants/rulesync-paths.js";
import {
  RulesyncRule,
  type RulesyncRuleFrontmatter,
  type RulesyncRuleFrontmatterInput,
  RulesyncRuleFrontmatterSchema,
} from "../features/rules/rulesync-rule.js";
import { formatError } from "../utils/error.js";
import {
  checkPathTraversal,
  ensureDir,
  listDirectoryFiles,
  removeFile,
  writeFileContent,
} from "../utils/file.js";
import { ConsoleLogger } from "../utils/logger.js";

const logger = new ConsoleLogger({ verbose: false, silent: true });

const maxRuleSizeBytes = 1024 * 1024; // 1MB
const maxRulesCount = 1000;

/**
 * Tool to list all rules from .rulesync/rules/*.md
 */
async function listRules(): Promise<
  Array<{
    relativePathFromCwd: string;
    frontmatter: RulesyncRuleFrontmatter;
  }>
> {
  const rulesDir = join(process.cwd(), RULESYNC_RULES_RELATIVE_DIR_PATH);

  try {
    const files = await listDirectoryFiles(rulesDir);
    const mdFiles = files.filter((file) => file.endsWith(".md"));

    const rules = await Promise.all(
      mdFiles.map(async (file) => {
        try {
          // Read the rule file using RulesyncRule
          const rule = await RulesyncRule.fromFile({
            relativeFilePath: file,
            validate: true,
          });

          const frontmatter = rule.getFrontmatter();

          return {
            relativePathFromCwd: join(RULESYNC_RULES_RELATIVE_DIR_PATH, file),
            frontmatter,
          };
        } catch (error) {
          logger.error(`Failed to read rule file ${file}: ${formatError(error)}`);
          return null;
        }
      }),
    );

    // Filter out null values (failed reads)
    return rules.filter((rule): rule is NonNullable<typeof rule> => rule !== null);
  } catch (error) {
    logger.error(
      `Failed to read rules directory (${RULESYNC_RULES_RELATIVE_DIR_PATH}): ${formatError(error)}`,
    );
    return [];
  }
}

/**
 * Tool to get detailed information about a specific rule
 */
async function getRule({ relativePathFromCwd }: { relativePathFromCwd: string }): Promise<{
  relativePathFromCwd: string;
  frontmatter: RulesyncRuleFrontmatter;
  body: string;
}> {
  checkPathTraversal({
    relativePath: relativePathFromCwd,
    intendedRootDir: process.cwd(),
  });

  const filename = basename(relativePathFromCwd);

  try {
    const rule = await RulesyncRule.fromFile({
      relativeFilePath: filename,
      validate: true,
    });

    return {
      relativePathFromCwd: join(RULESYNC_RULES_RELATIVE_DIR_PATH, filename),
      frontmatter: rule.getFrontmatter(),
      body: rule.getBody(),
    };
  } catch (error) {
    throw new Error(`Failed to read rule file ${relativePathFromCwd}: ${formatError(error)}`, {
      cause: error,
    });
  }
}

/**
 * Tool to create or update a rule (upsert operation)
 */
async function putRule({
  relativePathFromCwd,
  frontmatter,
  body,
}: {
  relativePathFromCwd: string;
  frontmatter: RulesyncRuleFrontmatterInput;
  body: string;
}): Promise<{
  relativePathFromCwd: string;
  frontmatter: RulesyncRuleFrontmatter;
  body: string;
}> {
  checkPathTraversal({
    relativePath: relativePathFromCwd,
    intendedRootDir: process.cwd(),
  });

  const filename = basename(relativePathFromCwd);

  // Check file size constraint
  const estimatedSize = JSON.stringify(frontmatter).length + body.length;
  if (estimatedSize > maxRuleSizeBytes) {
    throw new Error(
      `Rule size ${estimatedSize} bytes exceeds maximum ${maxRuleSizeBytes} bytes (1MB) for ${relativePathFromCwd}`,
    );
  }

  try {
    // Check rule count constraint
    const existingRules = await listRules();
    const isUpdate = existingRules.some(
      (rule) => rule.relativePathFromCwd === join(RULESYNC_RULES_RELATIVE_DIR_PATH, filename),
    );

    if (!isUpdate && existingRules.length >= maxRulesCount) {
      throw new Error(
        `Maximum number of rules (${maxRulesCount}) reached in ${RULESYNC_RULES_RELATIVE_DIR_PATH}`,
      );
    }

    // Create a new RulesyncRule instance
    const rule = new RulesyncRule({
      outputRoot: process.cwd(),
      relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
      relativeFilePath: filename,
      frontmatter,
      body,
      validate: true,
    });

    // Ensure directory exists
    const rulesDir = join(process.cwd(), RULESYNC_RULES_RELATIVE_DIR_PATH);
    await ensureDir(rulesDir);

    // Write the file
    await writeFileContent(rule.getFilePath(), rule.getFileContent());

    return {
      relativePathFromCwd: join(RULESYNC_RULES_RELATIVE_DIR_PATH, filename),
      frontmatter: rule.getFrontmatter(),
      body: rule.getBody(),
    };
  } catch (error) {
    throw new Error(`Failed to write rule file ${relativePathFromCwd}: ${formatError(error)}`, {
      cause: error,
    });
  }
}

/**
 * Tool to delete a rule
 */
async function deleteRule({ relativePathFromCwd }: { relativePathFromCwd: string }): Promise<{
  relativePathFromCwd: string;
}> {
  checkPathTraversal({
    relativePath: relativePathFromCwd,
    intendedRootDir: process.cwd(),
  });

  const filename = basename(relativePathFromCwd);
  const fullPath = join(process.cwd(), RULESYNC_RULES_RELATIVE_DIR_PATH, filename);

  try {
    await removeFile(fullPath);

    return {
      relativePathFromCwd: join(RULESYNC_RULES_RELATIVE_DIR_PATH, filename),
    };
  } catch (error) {
    throw new Error(`Failed to delete rule file ${relativePathFromCwd}: ${formatError(error)}`, {
      cause: error,
    });
  }
}

/**
 * Schema for rule-related tool parameters
 */
export const ruleToolSchemas = {
  listRules: z.object({}),
  getRule: z.object({
    relativePathFromCwd: z.string(),
  }),
  putRule: z.object({
    relativePathFromCwd: z.string(),
    frontmatter: RulesyncRuleFrontmatterSchema,
    body: z.string(),
  }),
  deleteRule: z.object({
    relativePathFromCwd: z.string(),
  }),
} as const;

/**
 * Tool definitions for rule-related operations
 */
export const ruleTools = {
  listRules: {
    name: "listRules",
    description: `List all rules from ${join(RULESYNC_RULES_RELATIVE_DIR_PATH, "*.md")} with their frontmatter.`,
    parameters: ruleToolSchemas.listRules,
    execute: async () => {
      const rules = await listRules();
      const output = { rules };
      return JSON.stringify(output, null, 2);
    },
  },
  getRule: {
    name: "getRule",
    description:
      "Get detailed information about a specific rule. relativePathFromCwd parameter is required.",
    parameters: ruleToolSchemas.getRule,
    execute: async (args: { relativePathFromCwd: string }) => {
      const result = await getRule({ relativePathFromCwd: args.relativePathFromCwd });
      return JSON.stringify(result, null, 2);
    },
  },
  putRule: {
    name: "putRule",
    description:
      "Create or update a rule (upsert operation). relativePathFromCwd, frontmatter, and body parameters are required.",
    parameters: ruleToolSchemas.putRule,
    execute: async (args: {
      relativePathFromCwd: string;
      frontmatter: RulesyncRuleFrontmatterInput;
      body: string;
    }) => {
      const result = await putRule({
        relativePathFromCwd: args.relativePathFromCwd,
        frontmatter: args.frontmatter,
        body: args.body,
      });
      return JSON.stringify(result, null, 2);
    },
  },
  deleteRule: {
    name: "deleteRule",
    description: "Delete a rule file. relativePathFromCwd parameter is required.",
    parameters: ruleToolSchemas.deleteRule,
    execute: async (args: { relativePathFromCwd: string }) => {
      const result = await deleteRule({ relativePathFromCwd: args.relativePathFromCwd });
      return JSON.stringify(result, null, 2);
    },
  },
} as const;
