import { join } from "node:path";

import { ConfigResolver } from "../../config/config-resolver.js";
import type { Feature, GitignoreDestination, RulesyncFeatures } from "../../types/features.js";
import type { ToolTarget } from "../../types/tool-targets.js";
import { fileExists, readFileContent, writeFileContent } from "../../utils/file.js";
import type { Logger } from "../../utils/logger.js";
import {
  ALL_GITIGNORE_ENTRIES,
  resolveGitignoreEntries,
  type ResolvedGitignoreEntry,
} from "./gitignore-entries.js";

const RULESYNC_HEADER = "# Generated by Rulesync";
const LEGACY_RULESYNC_HEADER = "# Generated by rulesync - AI tool configuration files";

const isRulesyncHeader = (line: string): boolean => {
  const trimmed = line.trim();
  return trimmed === RULESYNC_HEADER || trimmed === LEGACY_RULESYNC_HEADER;
};

const isRulesyncEntry = (line: string): boolean => {
  const trimmed = line.trim();
  if (trimmed === "" || isRulesyncHeader(line)) {
    return false;
  }
  return ALL_GITIGNORE_ENTRIES.includes(trimmed);
};

const removeExistingRulesyncEntries = (content: string): string => {
  const lines = content.split("\n");
  const filteredLines: string[] = [];
  let inRulesyncBlock = false;
  let consecutiveEmptyLines = 0;

  for (const line of lines) {
    const trimmed = line.trim();

    if (isRulesyncHeader(line)) {
      inRulesyncBlock = true;
      continue;
    }

    if (inRulesyncBlock) {
      if (trimmed === "") {
        consecutiveEmptyLines++;
        if (consecutiveEmptyLines >= 2) {
          inRulesyncBlock = false;
          consecutiveEmptyLines = 0;
        }
        continue;
      }

      if (isRulesyncEntry(line)) {
        consecutiveEmptyLines = 0;
        continue;
      }

      inRulesyncBlock = false;
      consecutiveEmptyLines = 0;
    }

    if (isRulesyncEntry(line)) {
      continue;
    }

    filteredLines.push(line);
  }

  let result = filteredLines.join("\n");

  while (result.endsWith("\n\n")) {
    result = result.slice(0, -1);
  }

  return result;
};

export type GitignoreCommandOptions = {
  readonly targets?: string[];
  readonly features?: RulesyncFeatures;
};

const groupEntriesByDestination = ({
  entries,
  resolveDestination,
}: {
  entries: ReadonlyArray<ResolvedGitignoreEntry>;
  resolveDestination: (target: ToolTarget, feature?: Feature | "general") => GitignoreDestination;
}): { gitignore: string[]; gitattributes: string[] } => {
  const gitignore = new Set<string>();
  const gitattributes = new Set<string>();

  for (const entry of entries) {
    const selectedToolTargets = entry.target.filter(
      (target): target is ToolTarget => target !== "common",
    );
    const destinations = new Set<GitignoreDestination>();
    for (const target of selectedToolTargets) {
      if (entry.feature === "general") {
        destinations.add(resolveDestination(target));
      } else {
        destinations.add(resolveDestination(target, entry.feature));
      }
    }

    if (destinations.has("gitattributes")) {
      gitattributes.add(entry.entry);
    }
    if (destinations.size === 0 || destinations.has("gitignore")) {
      gitignore.add(entry.entry);
    }
  }

  return {
    gitignore: [...gitignore],
    gitattributes: [...gitattributes],
  };
};

export const gitignoreCommand = async (
  logger: Logger,
  options?: GitignoreCommandOptions,
): Promise<void> => {
  const gitignorePath = join(process.cwd(), ".gitignore");
  const gitattributesPath = join(process.cwd(), ".gitattributes");
  const config = await ConfigResolver.resolve({});

  const resolvedEntries = resolveGitignoreEntries({
    targets: options?.targets,
    features: options?.features,
    logger,
  });
  const { gitignore: gitignoreEntries, gitattributes: gitattributesEntries } =
    groupEntriesByDestination({
      entries: resolvedEntries,
      resolveDestination: (target, feature) => {
        if (feature === undefined || feature === "general") {
          return config.getGitignoreDestination(target);
        }
        return config.getGitignoreDestination(target, feature);
      },
    });

  const updateRulesyncFile = async ({
    filePath,
    entries,
  }: {
    filePath: string;
    entries: string[];
  }): Promise<{
    updated: boolean;
    alreadyExistedEntries: string[];
    entriesToAdd: string[];
  }> => {
    let content = "";
    if (await fileExists(filePath)) {
      content = await readFileContent(filePath);
    }
    const cleanedContent = removeExistingRulesyncEntries(content);

    const existingEntries = new Set(
      content
        .split("\n")
        .map((line) => line.trim())
        .filter((line) => line !== "" && !isRulesyncHeader(line)),
    );
    const alreadyExistedEntries = entries.filter((entry) => existingEntries.has(entry));
    const entriesToAdd = entries.filter((entry) => !existingEntries.has(entry));
    const rulesyncBlock = [RULESYNC_HEADER, ...entries].join("\n");
    const newContent =
      entries.length === 0
        ? cleanedContent.trim()
          ? `${cleanedContent.trimEnd()}\n`
          : ""
        : cleanedContent.trim()
          ? `${cleanedContent.trimEnd()}\n\n${rulesyncBlock}\n`
          : `${rulesyncBlock}\n`;

    if (content === newContent) {
      return { updated: false, alreadyExistedEntries, entriesToAdd: [] };
    }
    await writeFileContent(filePath, newContent);
    return { updated: true, alreadyExistedEntries, entriesToAdd };
  };

  const gitignoreResult = await updateRulesyncFile({
    filePath: gitignorePath,
    entries: gitignoreEntries,
  });
  const gitattributesResult = await updateRulesyncFile({
    filePath: gitattributesPath,
    entries: gitattributesEntries,
  });

  if (!gitignoreResult.updated && !gitattributesResult.updated) {
    // Capture JSON data if in JSON mode
    if (logger.jsonMode) {
      logger.captureData("entriesAdded", []);
      logger.captureData("gitignorePath", gitignorePath);
      logger.captureData("gitattributesPath", gitattributesPath);
      logger.captureData("alreadyExisted", [...gitignoreEntries, ...gitattributesEntries]);
    }
    logger.success(".gitignore / .gitattributes are already up to date");
    return;
  }

  // Capture JSON data if in JSON mode
  if (logger.jsonMode) {
    logger.captureData("entriesAdded", [
      ...gitignoreResult.entriesToAdd,
      ...gitattributesResult.entriesToAdd,
    ]);
    logger.captureData("gitignorePath", gitignorePath);
    logger.captureData("gitattributesPath", gitattributesPath);
    logger.captureData("alreadyExisted", [
      ...gitignoreResult.alreadyExistedEntries,
      ...gitattributesResult.alreadyExistedEntries,
    ]);
  }

  if (gitignoreResult.updated) {
    logger.success("Updated .gitignore with rulesync entries:");
  } else {
    logger.success(".gitignore is already up to date");
  }
  for (const entry of gitignoreEntries) {
    logger.info(`  ${entry}`);
  }
  if (gitattributesEntries.length > 0) {
    if (gitattributesResult.updated) {
      logger.success("Updated .gitattributes with rulesync entries:");
    } else {
      logger.success(".gitattributes is already up to date");
    }
    for (const entry of gitattributesEntries) {
      logger.info(`  ${entry}`);
    }
  }

  logger.info("");
  logger.info(
    "💡 If you're using Google Antigravity, note that rules, workflows, and skills won't load if they're gitignored.",
  );
  logger.info("   You can add the following to .git/info/exclude instead:");
  logger.info("   **/.agent/rules/");
  logger.info("   **/.agent/workflows/");
  logger.info("   **/.agent/skills/");
  logger.info("   For more details: https://github.com/dyoshikawa/rulesync/issues/981");
};
