import { GUARD_RULES, FORBIDDEN_IMPORTS, type GuardViolation } from "./rules";
import { runContractGuardCheck } from "./contract-guard";
import { validateSlotContent } from "../slot/validator";
import type { RoutesManifest } from "../spec/schema";
import type { GeneratedMap } from "../generator/generate";
import { loadManduConfig, type GuardRuleSeverity } from "../config";
import { extractImportsAST, extractExportsAST } from "./ast-analyzer";
import {
  validateCustomRules,
  type GuardRule as CustomGuardRule,
  type GuardRuleContext as CustomGuardRuleContext,
  type GuardViolation as CustomGuardViolation,
} from "./define-rule";
import type { ManduConfig } from "../config/mandu";
import path from "path";
import fs from "fs/promises";

export interface GuardCheckResult {
  passed: boolean;
  violations: GuardViolation[];
}

/**
 * Canonical docs URL for the `getGenerated()` runtime-registry pattern.
 * Shared by the Guard `INVALID_GENERATED_IMPORT` rule and the bundler
 * plugin `block-generated-imports` so both paths surface the same
 * remediation target.
 */
export const GENERATED_IMPORT_DOCS_URL =
  "https://mandujs.com/docs/architect/generated-access";

/**
 * Build the user-facing message for a detected direct generated-artifact
 * import. `specifier` is the literal import string that tripped the guard
 * (not the resolved path).
 *
 * This helper is the single source of truth for the message text — both
 * the static Guard pass (`checkInvalidGeneratedImport`) and the bundler
 * plugin (`blockGeneratedImports`) call through it so the two paths
 * cannot drift.
 */
export function buildForbiddenGeneratedImportMessage(specifier: string): string {
  return (
    `Direct generated artifact imports are forbidden: ${specifier}. ` +
    `Use the runtime registry: see ${GENERATED_IMPORT_DOCS_URL}`
  );
}

/**
 * Shared remediation hint. Points at `getGenerated()` from
 * `@mandujs/core/runtime` and the decision-tree docs.
 */
export const FORBIDDEN_GENERATED_IMPORT_SUGGESTION =
  "Import getGenerated() from @mandujs/core/runtime and read the generated artifact through the manifest. " +
  `See ${GENERATED_IMPORT_DOCS_URL} for the decision tree.`;

function normalizeSeverity(level: GuardRuleSeverity): "error" | "warning" | "off" {
  if (level === "warn") return "warning";
  return level;
}

function applyRuleSeverity(
  violations: GuardViolation[],
  config: { rules?: Record<string, GuardRuleSeverity>; contractRequired?: GuardRuleSeverity }
): GuardViolation[] {
  const resolved: GuardViolation[] = [];
  const ruleOverrides = config.rules ?? {};

  for (const violation of violations) {
    let override = ruleOverrides[violation.ruleId];
    if (violation.ruleId === "CONTRACT_MISSING" && config.contractRequired) {
      override = config.contractRequired;
    }

    const baseSeverity = violation.severity ?? GUARD_RULES[violation.ruleId]?.severity ?? "error";
    const finalSeverity = override ? normalizeSeverity(override) : baseSeverity;

    if (finalSeverity === "off") continue;

    resolved.push({ ...violation, severity: finalSeverity });
  }

  return resolved;
}

async function fileExists(filePath: string): Promise<boolean> {
  try {
    await fs.access(filePath);
    return true;
  } catch {
    return false;
  }
}

async function readFileContent(filePath: string): Promise<string | null> {
  try {
    return await Bun.file(filePath).text();
  } catch {
    return null;
  }
}

// Rule 2: Generated file manual edit detection
export async function checkGeneratedManualEdit(
  rootDir: string,
  generatedMap: GeneratedMap
): Promise<GuardViolation[]> {
  const violations: GuardViolation[] = [];

  for (const [filePath, meta] of Object.entries(generatedMap.files)) {
    const fullPath = path.join(rootDir, filePath);
    const content = await readFileContent(fullPath);

    if (!content) continue;

    // Check if the "DO NOT EDIT" comment was removed or modified
    if (!content.includes("Generated by Mandu - DO NOT EDIT DIRECTLY")) {
      violations.push({
        ruleId: GUARD_RULES.GENERATED_MANUAL_EDIT.id,
        file: filePath,
        message: `generated 파일이 수동으로 변경된 것으로 보입니다 (routeId: ${meta.routeId})`,
        suggestion: "bunx mandu generate를 실행하여 파일을 재생성하세요",
      });
    }
  }

  return violations;
}

// Rule 3: Non-generated importing generated
export async function checkInvalidGeneratedImport(
  rootDir: string
): Promise<GuardViolation[]> {
  const violations: GuardViolation[] = [];

  // Scan non-generated source files
  const sourceDirs = [
    path.join(rootDir, "packages"),
    path.join(rootDir, "src"),
    path.join(rootDir, "app"),
  ];

  for (const sourceDir of sourceDirs) {
    const files = await scanTsFiles(sourceDir);

    for (const file of files) {
      // Skip generated directories
      if (file.includes("/generated/") || file.includes("\\generated\\")) {
        continue;
      }

      const content = await readFileContent(file);
      if (!content) continue;

      // Check for imports from generated directories
      const importRegex = /import\s+.*from\s+['"](.*generated.*)['"]/g;
      let match;

      while ((match = importRegex.exec(content)) !== null) {
        const relativePath = path.relative(rootDir, file);
        violations.push({
          ruleId: GUARD_RULES.INVALID_GENERATED_IMPORT.id,
          file: relativePath,
          message: buildForbiddenGeneratedImportMessage(match[1]),
          suggestion: FORBIDDEN_GENERATED_IMPORT_SUGGESTION,
        });
      }
    }
  }

  return violations;
}

// Rule 5: Slot file existence check
export async function checkSlotFileExists(
  manifest: RoutesManifest,
  rootDir: string
): Promise<GuardViolation[]> {
  const violations: GuardViolation[] = [];

  for (const route of manifest.routes) {
    if (route.slotModule) {
      const slotPath = path.join(rootDir, route.slotModule);
      const exists = await fileExists(slotPath);

      if (!exists) {
        violations.push({
          ruleId: GUARD_RULES.SLOT_NOT_FOUND.id,
          file: route.slotModule,
          message: `Slot 파일을 찾을 수 없습니다 (routeId: ${route.id})`,
          suggestion: "bunx mandu generate를 실행하여 slot 파일을 생성하세요",
        });
      }
    }
  }

  return violations;
}

// Rule 6: Slot content validation (신규)
export async function checkSlotContentValidation(
  manifest: RoutesManifest,
  rootDir: string
): Promise<GuardViolation[]> {
  const violations: GuardViolation[] = [];

  for (const route of manifest.routes) {
    if (!route.slotModule) continue;

    const slotPath = path.join(rootDir, route.slotModule);
    const content = await readFileContent(slotPath);

    if (!content) continue; // File doesn't exist, handled by checkSlotFileExists

    const validationResult = validateSlotContent(content);

    // Convert slot validation issues to guard violations
    for (const issue of validationResult.issues) {
      if (issue.severity === "error") {
        // Map slot issue codes to guard rule IDs
        let ruleId = "SLOT_VALIDATION_ERROR";
        if (issue.code === "MISSING_DEFAULT_EXPORT") {
          ruleId = GUARD_RULES.SLOT_MISSING_DEFAULT_EXPORT?.id ?? "SLOT_MISSING_DEFAULT_EXPORT";
        } else if (issue.code === "NO_RESPONSE_PATTERN" || issue.code === "INVALID_HANDLER_RETURN") {
          ruleId = GUARD_RULES.SLOT_INVALID_RETURN?.id ?? "SLOT_INVALID_RETURN";
        } else if (issue.code === "MISSING_FILLING_PATTERN") {
          ruleId = GUARD_RULES.SLOT_MISSING_FILLING_PATTERN?.id ?? "SLOT_MISSING_FILLING_PATTERN";
        } else if (issue.code === "ZOD_DIRECT_IMPORT") {
          ruleId = GUARD_RULES.SLOT_ZOD_DIRECT_IMPORT?.id ?? "SLOT_ZOD_DIRECT_IMPORT";
        }

        violations.push({
          ruleId,
          file: route.slotModule,
          message: `[${route.id}] ${issue.message}`,
          suggestion: issue.suggestion,
          line: issue.line,
          severity: issue.severity,
        });
      }
    }
  }

  return violations;
}

// Rule: Island-First Integrity
export async function checkIslandFirstIntegrity(
  manifest: RoutesManifest,
  rootDir: string
): Promise<GuardViolation[]> {
  const violations: GuardViolation[] = [];

  for (const route of manifest.routes) {
    if (route.kind !== "page" || !route.clientModule) continue;

    // 1. clientModule 파일 존재 여부
    const clientPath = path.join(rootDir, route.clientModule);
    if (!(await fileExists(clientPath))) {
      violations.push({
        ruleId: "CLIENT_MODULE_NOT_FOUND",
        file: route.clientModule,
        message: `clientModule 파일을 찾을 수 없습니다 (routeId: ${route.id})`,
        suggestion: "clientModule 경로를 확인하거나 파일을 생성하세요",
      });
      continue;
    }

    // 2. Island-First integrity: verify that a .island.tsx or .client.tsx file
    //    exists alongside the page's componentModule. The page does NOT need to
    //    directly import the island - the framework auto-links them via
    //    data-island attributes and the manifest's clientModule field.
    if (route.componentModule && route.clientModule) {
      const componentPath = path.join(rootDir, route.componentModule);
      const content = await readFileContent(componentPath);
      // Check if the island file actually exists on disk
      const clientPath = path.join(rootDir, route.clientModule);
      const clientExists = await fileExists(clientPath);
      if (content && !clientExists && !content.includes("data-island") && !content.includes("data-mandu-island")) {
        violations.push({
          ruleId: "ISLAND_FIRST_INTEGRITY",
          file: route.componentModule,
          message: `No island file found for page route (routeId: ${route.id}). The clientModule '${route.clientModule}' does not exist.`,
          suggestion:
            "Create a .island.tsx file in the same app/ directory as page.tsx. " +
            "The page should reference islands via data-island attributes, NOT by directly importing or re-exporting the island. " +
            "Importing island() return values into page.tsx causes a runtime crash because they are config objects, not React components.",
        });
      }
    }
  }

  return violations;
}

// Rule 4: Forbidden imports in generated files
export async function checkForbiddenImportsInGenerated(
  rootDir: string,
  generatedMap: GeneratedMap
): Promise<GuardViolation[]> {
  const violations: GuardViolation[] = [];

  for (const [filePath] of Object.entries(generatedMap.files)) {
    const fullPath = path.join(rootDir, filePath);
    const content = await readFileContent(fullPath);

    if (!content) continue;

    for (const forbidden of FORBIDDEN_IMPORTS) {
      const importRegex = new RegExp(
        `import\\s+.*from\\s+['"]${forbidden}['"]|require\\s*\\(\\s*['"]${forbidden}['"]\\s*\\)`,
        "g"
      );

      if (importRegex.test(content)) {
        violations.push({
          ruleId: GUARD_RULES.FORBIDDEN_IMPORT_IN_GENERATED.id,
          file: filePath,
          message: `generated 파일에서 금지된 모듈 '${forbidden}' import 감지`,
          suggestion: `'${forbidden}' 모듈 사용이 필요하면 slot 로직에서 처리하세요`,
        });
      }
    }
  }

  return violations;
}

async function scanTsFiles(dir: string): Promise<string[]> {
  const files: string[] = [];

  try {
    const entries = await fs.readdir(dir, { withFileTypes: true });

    for (const entry of entries) {
      const fullPath = path.join(dir, entry.name);

      if (entry.isDirectory()) {
        if (entry.name !== "node_modules" && entry.name !== "dist") {
          const subFiles = await scanTsFiles(fullPath);
          files.push(...subFiles);
        }
      } else if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx"))) {
        files.push(fullPath);
      }
    }
  } catch {
    // Directory doesn't exist or can't be read
  }

  return files;
}

// Rule: spec/ directory naming convention scan
export async function checkSpecDirNaming(
  rootDir: string
): Promise<GuardViolation[]> {
  const violations: GuardViolation[] = [];

  // Check spec/slots/ — only .slot.ts allowed
  const slotsDir = path.join(rootDir, "spec/slots");
  try {
    const files = await fs.readdir(slotsDir);
    for (const file of files) {
      if (file.endsWith(".ts") && !file.endsWith(".slot.ts")) {
        violations.push({
          ruleId: GUARD_RULES.SLOT_DIR_INVALID_FILE.id,
          file: `spec/slots/${file}`,
          message: `spec/slots/에 .slot.ts가 아닌 파일: ${file}`,
          suggestion: `.slot.ts로 이름을 바꾸거나, 이 파일이 client slot이면 spec/slots/로 .client.ts 접미사로 이동하세요`,
        });
      }
    }
  } catch {}

  // Check spec/contracts/ — only .contract.ts allowed
  const contractsDir = path.join(rootDir, "spec/contracts");
  try {
    const files = await fs.readdir(contractsDir);
    for (const file of files) {
      if (file.endsWith(".ts") && !file.endsWith(".contract.ts")) {
        violations.push({
          ruleId: GUARD_RULES.CONTRACT_DIR_INVALID_FILE.id,
          file: `spec/contracts/${file}`,
          message: `spec/contracts/에 .contract.ts가 아닌 파일: ${file}`,
          suggestion: `.contract.ts로 이름을 바꾸세요`,
        });
      }
    }
  } catch {}

  return violations;
}

export async function runGuardCheck(
  manifest: RoutesManifest,
  rootDir: string
): Promise<GuardCheckResult> {
  const config = await loadManduConfig(rootDir);

  const mapPath = path.join(rootDir, ".mandu/generated/generated.map.json");

  // ============================================
  // Phase 1: 독립적인 검사 병렬 실행
  // ============================================
  const [
    importViolations,
    slotViolations,
    specDirViolations,
    islandViolations,
  ] = await Promise.all([
    checkInvalidGeneratedImport(rootDir),
    checkSlotFileExists(manifest, rootDir),
    checkSpecDirNaming(rootDir),
    checkIslandFirstIntegrity(manifest, rootDir),
  ]);

  const violations: GuardViolation[] = [];
  violations.push(...importViolations);
  violations.push(...slotViolations);
  violations.push(...specDirViolations);
  violations.push(...islandViolations);

  // ============================================
  // Phase 2: generatedMap 의존 검사
  // ============================================
  let generatedMap: GeneratedMap | null = null;
  if (await fileExists(mapPath)) {
    try {
      const mapContent = await Bun.file(mapPath).text();
      generatedMap = JSON.parse(mapContent);
    } catch {
      // Map file corrupted or missing
    }
  }

  if (generatedMap) {
    const [editViolations, forbiddenViolations] = await Promise.all([
      checkGeneratedManualEdit(rootDir, generatedMap),
      checkForbiddenImportsInGenerated(rootDir, generatedMap),
    ]);
    violations.push(...editViolations);
    violations.push(...forbiddenViolations);
  }

  // ============================================
  // Phase 3: Slot + Contract 검사 병렬
  // ============================================
  const [slotContentViolations, contractViolations] = await Promise.all([
    checkSlotContentValidation(manifest, rootDir),
    runContractGuardCheck(manifest, rootDir),
  ]);
  violations.push(...slotContentViolations);
  violations.push(...contractViolations);

  // ============================================
  // Issue #245 — DESIGN.md-driven inline-class enforcement.
  // Skipped silently when guard.design is undefined or has nothing
  // to enforce (no explicit forbid list + autoFromDesignMd off).
  // ============================================
  const designConfig = (config.guard as { design?: unknown } | undefined)?.design;
  if (designConfig && typeof designConfig === "object") {
    const { checkDesignInlineClasses } = await import("./design-inline-class");
    const designViolations = await checkDesignInlineClasses(
      rootDir,
      designConfig as Parameters<typeof checkDesignInlineClasses>[1],
    );
    violations.push(...designViolations);
  }

  // ============================================
  // Phase 18.ν — Consumer-defined custom rules
  // ============================================
  const customRules = (config.guard?.rules as unknown);
  if (Array.isArray(customRules) && customRules.length > 0) {
    const customViolations = await runCustomRules(
      customRules as CustomGuardRule[],
      rootDir,
      config
    );
    violations.push(...customViolations);
  }

  const resolvedViolations = applyRuleSeverity(violations, normalizeGuardConfigForSeverity(config.guard));
  const passed = resolvedViolations.every((v) => v.severity !== "error");

  return {
    passed,
    violations: resolvedViolations,
  };
}

// ═══════════════════════════════════════════════════════════════════════════
// Phase 18.ν — Consumer-defined Guard rules
// ═══════════════════════════════════════════════════════════════════════════

/**
 * The existing `config.guard.rules` field was a `Record<string,
 * GuardRuleSeverity>` map for built-in rule overrides. Phase 18.ν
 * allows consumers to also pass an array of `GuardRule` objects.
 * `applyRuleSeverity` only understands the record shape, so we strip
 * the array out before forwarding to it.
 */
function normalizeGuardConfigForSeverity(
  guard: ManduConfig["guard"] | undefined
): { rules?: Record<string, GuardRuleSeverity>; contractRequired?: GuardRuleSeverity } {
  if (!guard) return {};
  const rulesField = guard.rules as unknown;
  const rules =
    rulesField && !Array.isArray(rulesField) && typeof rulesField === "object"
      ? (rulesField as Record<string, GuardRuleSeverity>)
      : undefined;
  return {
    rules,
    contractRequired: (guard as { contractRequired?: GuardRuleSeverity }).contractRequired,
  };
}

/**
 * Parallel scan cap for `runCustomRules()`. Matches `safeBuild` default
 * (`MANDU_BUN_BUILD_CONCURRENCY`) semantics — tunable via
 * `MANDU_GUARD_CUSTOM_CONCURRENCY` positive integer env var.
 */
function customRuleConcurrency(): number {
  const raw = process.env.MANDU_GUARD_CUSTOM_CONCURRENCY;
  if (!raw) return 8;
  const parsed = Number.parseInt(raw, 10);
  if (!Number.isFinite(parsed) || parsed < 1) return 8;
  return parsed;
}

/** Default source directories scanned for custom rules. */
const CUSTOM_RULE_SOURCE_DIRS = ["packages", "src", "app"];

async function collectCustomRuleSourceFiles(rootDir: string): Promise<string[]> {
  const files = await Promise.all(
    CUSTOM_RULE_SOURCE_DIRS.map((d) => scanTsFiles(path.join(rootDir, d)))
  );
  // Deduplicate paths (a file could in theory live under multiple roots
  // if a consumer symlinks, which the walker doesn't follow but we
  // guard anyway).
  return Array.from(new Set(files.flat()));
}

/**
 * Convert a consumer-defined `GuardViolation` into the standard Guard
 * report `GuardViolation` shape. Adds the `custom:<id>` ruleId prefix
 * so the reporter can attribute each entry unambiguously.
 */
function toReportViolation(
  rule: CustomGuardRule,
  violation: CustomGuardViolation
): GuardViolation {
  const severity: "error" | "warning" =
    rule.severity === "error" ? "error" : "warning";
  return {
    ruleId: `custom:${rule.id}`,
    file: violation.file,
    message: violation.message,
    suggestion: violation.hint ?? violation.docsUrl ?? "",
    line: violation.line,
    severity,
  };
}

/**
 * Execute every consumer-defined rule against every source file under
 * `rootDir`. Rules are awaited per-file with a concurrency cap; a
 * single rule throwing is caught and reported as a `custom:<id>`
 * violation so the rest of the scan still completes.
 *
 * Exported for tests; not re-exported via `guard/index.ts` to keep the
 * public surface small.
 */
export async function runCustomRules(
  rules: readonly CustomGuardRule[],
  rootDir: string,
  config: ManduConfig
): Promise<GuardViolation[]> {
  const { duplicates, malformed } = validateCustomRules(rules);
  const results: GuardViolation[] = [];

  if (malformed.length > 0) {
    for (const idx of malformed) {
      results.push({
        ruleId: "custom:__invalid__",
        file: "mandu.config",
        message: `guard.rules[${idx}] is not a valid GuardRule (missing \`id\` or \`check()\`).`,
        suggestion:
          "Wrap the rule with defineGuardRule({...}) or import a preset from @mandujs/core/guard/define-rule.",
        severity: "error",
      });
    }
  }

  if (duplicates.length > 0) {
    for (const dup of duplicates) {
      // Warning-level so duplicates don't gate CI but still surface in the
      // report. Matches the "soft failure" convention used by config-guard.
      results.push({
        ruleId: `custom:${dup.id}`,
        file: "mandu.config",
        message: `Duplicate guard.rules id "${dup.id}" at indices [${dup.indices.join(", ")}] — only the first instance will be enforced.`,
        suggestion: "Give each rule a unique \`id\`, or remove the duplicate.",
        severity: "warning",
      });
    }
  }

  // Deduplicate rules by id (keep first occurrence). Matches the
  // warning emitted above.
  const seen = new Set<string>();
  const uniqueRules = rules.filter((r) => {
    if (!r || typeof r !== "object" || typeof (r as CustomGuardRule).id !== "string") return false;
    const id = (r as CustomGuardRule).id;
    if (seen.has(id)) return false;
    seen.add(id);
    return true;
  });
  if (uniqueRules.length === 0) return results;

  const files = await collectCustomRuleSourceFiles(rootDir);
  if (files.length === 0) return results;

  const cap = customRuleConcurrency();
  let cursor = 0;

  async function worker(): Promise<GuardViolation[]> {
    const local: GuardViolation[] = [];
    while (true) {
      const index = cursor++;
      if (index >= files.length) return local;
      const filePath = files[index];

      // Skip __generated__ and build output.
      if (filePath.includes("__generated__") || filePath.includes(".mandu/")) continue;

      let content: string;
      try {
        content = await Bun.file(filePath).text();
      } catch {
        continue;
      }

      let imports: ReturnType<typeof extractImportsAST>;
      let exportsAst: ReturnType<typeof extractExportsAST>;
      try {
        imports = extractImportsAST(content);
        exportsAst = extractExportsAST(content);
      } catch {
        // Tokenizer failure on an exotic file — skip rather than abort.
        continue;
      }

      const relFile = path.relative(rootDir, filePath) || filePath;
      const ctx: CustomGuardRuleContext = {
        sourceFile: relFile.replace(/\\/g, "/"),
        content,
        imports,
        exports: exportsAst,
        config,
        projectRoot: rootDir,
      };

      for (const rule of uniqueRules) {
        try {
          const violations = await rule.check(ctx);
          if (!Array.isArray(violations)) continue;
          for (const v of violations) {
            if (!v || typeof v !== "object") continue;
            local.push(toReportViolation(rule, v));
          }
        } catch (err) {
          const message = err instanceof Error ? err.message : String(err);
          local.push({
            ruleId: `custom:${rule.id}`,
            file: ctx.sourceFile,
            message: `Rule "${rule.id}" threw: ${message}`,
            suggestion: "Fix the rule's check() implementation or wrap it in a try/catch.",
            severity: rule.severity === "error" ? "error" : "warning",
          });
        }
      }
    }
  }

  const workerCount = Math.min(cap, files.length);
  const workers: Promise<GuardViolation[]>[] = [];
  for (let i = 0; i < workerCount; i++) workers.push(worker());
  const chunks = await Promise.all(workers);
  for (const chunk of chunks) results.push(...chunk);

  return results;
}
