import { glob, readFile, stat, writeFile } from "fs/promises";
import { join } from "path";
import { resolveConfig } from "prettier";
import { PrettierParserError } from "../formatter/parser.js";
import { checkFormat, format, getFormatterFromFilename } from "./formatter.js";
import { createDiagnostic } from "./messages.js";
import { normalizePath } from "./path-utils.js";
import { Diagnostic, NoTarget } from "./types.js";

export interface TypeSpecFormatOptions {
  exclude?: string[];
}

/**  The paths of which are either relative or absolute based on the original file path patterns. */
export interface TypeSpecFormatResult {
  /** Files which were formatted successfully, */
  readonly formatted: string[];
  /** Files which had a valid format already. */
  readonly alreadyFormatted: string[];
  /** Files that were included in the filter but are not in the scope of the typespec formatter. */
  readonly ignored: string[];
  /** Files with errors */
  readonly errored: [string, Diagnostic][];
}

/**
 * Format all the TypeSpec project files(.tsp, tspconfig.yaml).
 * @param patterns List of wildcard pattern searching for TypeSpec files.
 * @returns list of files which failed to format.
 */
export async function formatFiles(
  patterns: string[],
  { exclude }: TypeSpecFormatOptions,
): Promise<TypeSpecFormatResult> {
  const files = await findFiles(patterns, exclude);
  const errored: [string, Diagnostic][] = [];
  const formatted: string[] = [];
  const alreadyFormatted: string[] = [];
  const ignored: string[] = [];
  for (const file of files) {
    const result = await formatFile(file);
    switch (result.kind) {
      case "formatted":
        formatted.push(file);
        break;
      case "already-formatted":
        alreadyFormatted.push(file);
        break;
      case "ignored":
        ignored.push(file);
        break;
      case "error":
        errored.push([file, result.diagnostic]);
        break;
    }
  }

  return { formatted, ignored, errored, alreadyFormatted };
}

export interface CheckFilesFormatResult {
  readonly formatted: string[];
  readonly needsFormat: string[];
  readonly ignored: string[];
  readonly errored: [string, Diagnostic][];
}
/**
 * Check the format of the files in the given pattern.
 */
export async function checkFilesFormat(
  patterns: string[],
  { exclude }: TypeSpecFormatOptions,
): Promise<CheckFilesFormatResult> {
  const files = await findFiles(patterns, exclude);
  const errored: [string, Diagnostic][] = [];
  const formatted: string[] = [];
  const needsFormat: string[] = [];
  const ignored: string[] = [];
  for (const file of files) {
    const result = await checkFileFormat(file);
    switch (result.kind) {
      case "formatted":
        formatted.push(file);
        break;
      case "needs-format":
        needsFormat.push(file);
        break;
      case "ignored":
        ignored.push(file);
        break;
      case "error":
        errored.push([file, result.diagnostic]);
        break;
    }
  }

  return { formatted, needsFormat, ignored, errored };
}

export type FormatFileResult =
  /** File formatted successfully. */
  | { kind: "formatted" }
  /** File was already formatted. */
  | { kind: "already-formatted" }
  /** File is not in a format that can be formatted by TypeSpec */
  | { kind: "ignored" }
  /** Error occurred, probably a parsing error. */
  | { kind: "error"; diagnostic: Diagnostic };

export async function formatFile(filename: string): Promise<FormatFileResult> {
  const content = await readFile(filename, "utf-8");
  const prettierConfig = await resolveConfig(filename);
  const formatter = getFormatterFromFilename(filename);
  if (formatter === undefined) {
    return { kind: "ignored" };
  }
  try {
    const formattedContent = await format(content, formatter, prettierConfig ?? {});
    if (formattedContent === content) {
      return { kind: "already-formatted" };
    }
    await writeFile(filename, formattedContent);
    return { kind: "formatted" };
  } catch (e) {
    if (e instanceof PrettierParserError) {
      return {
        kind: "error",
        diagnostic: createDiagnostic({
          code: "format-failed",
          format: { file: filename, details: e.message },
          target: NoTarget,
        }),
      };
    } else {
      throw e;
    }
  }
}

export type CheckFormatResult =
  /** File formatted successfully. */
  | { kind: "formatted" }
  /** File needs format */
  | { kind: "needs-format" }
  /** File is not in a format that can be formatted by TypeSpec */
  | { kind: "ignored" }
  /** Error occurred, probably a parsing error. */
  | { kind: "error"; diagnostic: Diagnostic };

/**
 * Check the given TypeSpec file is correctly formatted.
 */
export async function checkFileFormat(filename: string): Promise<CheckFormatResult> {
  const content = await readFile(filename, "utf-8");
  const prettierConfig = await resolveConfig(filename);
  const formatter = getFormatterFromFilename(filename);
  if (formatter === undefined) {
    return { kind: "ignored" };
  }
  try {
    const formatted = await checkFormat(content, formatter, prettierConfig ?? {});
    return { kind: formatted ? "formatted" : "needs-format" };
  } catch (e) {
    if (e instanceof PrettierParserError) {
      return {
        kind: "error",
        diagnostic: createDiagnostic({
          code: "format-failed",
          format: { file: filename, details: e.message },
          target: NoTarget,
        }),
      };
    } else {
      throw e;
    }
  }
}

async function findFiles(include: string[], ignore: string[] = []): Promise<string[]> {
  const expandedInclude = await expandDirectoryPatterns(include);
  const results: string[] = [];
  for await (const entry of glob(expandedInclude, {
    withFileTypes: true,
    exclude: ["**/node_modules", ...ignore],
  })) {
    if (entry.isFile()) {
      results.push(join(entry.parentPath, entry.name));
    }
  }
  return results;
}

/**
 * Expand bare directory paths to glob patterns.
 * A directory "src" becomes both "src" and "src/**\/*" so it matches the
 * directory entry itself (for exclude short-circuiting) and its contents.
 * Glob patterns ending in "/**" also get the bare directory form added.
 */
async function expandDirectoryPatterns(patterns: string[]): Promise<string[]> {
  const expanded: string[] = [];
  for (const pattern of patterns) {
    if (/[*?{[]/.test(pattern)) {
      expanded.push(normalizePath(pattern));
      // Also match the directory itself so exclude can short-circuit traversal
      const normalized = normalizePath(pattern);
      if (normalized.endsWith("/**/*")) {
        expanded.push(normalized.slice(0, -4));
      } else if (normalized.endsWith("/**")) {
        expanded.push(normalized.slice(0, -3));
      }
    } else {
      try {
        if ((await stat(pattern)).isDirectory()) {
          expanded.push(normalizePath(pattern));
          expanded.push(normalizePath(`${pattern}/**/*`));
          continue;
        }
      } catch {
        // not a valid path — treat as glob pattern
      }
      expanded.push(normalizePath(pattern));
    }
  }
  return expanded;
}
