import { readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync } from "node:fs";
import { join, resolve, dirname } from "node:path";
import { homedir } from "node:os";
import type { AnalyticsBatch, WeeklyReport, MonthlyReport, PostAnalytics } from "../models/types.js";

/**
 * Walk up from `startDir` until a directory containing `.claude-plugin/plugin.json`
 * is found. Returns that directory, or null if no marker is found before the
 * filesystem root.
 *
 * Anchoring on the plugin marker locates the plugin tree correctly by
 * construction regardless of whether this module runs from `src/utils/`
 * (under tsx) or `build/utils/` (compiled) — and survives a future source
 * move that a hardcoded "../../../../" count would silently break. Since M0 it
 * locates bundled read-only assets only; the per-user data root is external
 * (getDataRoot), no longer derived from the plugin tree.
 */
export function findPluginRoot(startDir: string): string | null {
  let dir = resolve(startDir);

  // dirname() of the filesystem root returns the root itself; stop when it
  // no longer changes.
  for (;;) {
    if (existsSync(join(dir, ".claude-plugin", "plugin.json"))) {
      return dir;
    }
    const parent = dirname(dir);
    if (parent === dir) {
      return null;
    }
    dir = parent;
  }
}

/**
 * Resolve HOME with the codebase idiom, but never fall through to "" — that would
 * turn the absolute data path into a CWD-relative one. os.homedir() is the floor.
 */
function resolveHome(): string {
  return process.env.HOME || process.env.USERPROFILE || homedir();
}

/**
 * CANONICAL — twin of hooks/scripts/data-root.mjs:getDataRoot. MUST STAY IN SYNC:
 * identical default (~/.claude/linkedin-studio) + identical LINKEDIN_STUDIO_DATA
 * override semantics. A twin exists (not a shared import) because the runtimes
 * differ: the hooks are plain .mjs (no tsx) and cannot import this TS module. The
 * twin-consistency test (hooks/scripts/__tests__/data-root.test.mjs) guards the
 * contract behaviorally.
 *
 * Per-user data root: ~/.claude/linkedin-studio/<subdir>, overridable via
 * LINKEDIN_STUDIO_DATA (test + power-user seam, mirrors the state-file location).
 */
export function getDataRoot(subdir: string): string {
  const dataBase =
    process.env.LINKEDIN_STUDIO_DATA ||
    join(resolveHome(), ".claude", "linkedin-studio");
  return join(dataBase, subdir);
}

/**
 * Get the analytics root directory — since M0 the external per-user dir
 * (getDataRoot("analytics")). The deprecated ANALYTICS_ROOT env override still
 * wins for back-compat (D4) so cli.ts and existing prose keep working through the
 * migration. findPluginRoot is preserved (above) for bundled read-only assets.
 */
export function getAnalyticsRoot(): string {
  if (process.env.ANALYTICS_ROOT) {
    return resolve(process.env.ANALYTICS_ROOT);
  }
  return getDataRoot("analytics");
}

/**
 * Ensure required subdirectories exist under analytics root
 */
export function ensureDirectories(root: string): void {
  const directories = ["exports", "posts", "weekly-reports", "monthly-reports"];

  if (!existsSync(root)) {
    mkdirSync(root, { recursive: true });
  }

  for (const dir of directories) {
    const path = join(root, dir);
    if (!existsSync(path)) {
      mkdirSync(path, { recursive: true });
    }
  }
}

/**
 * List all CSV export files in the exports directory
 */
export function listExports(root: string): string[] {
  const exportsDir = join(root, "exports");

  if (!existsSync(exportsDir)) {
    return [];
  }

  return readdirSync(exportsDir)
    .filter(file => file.endsWith(".csv"))
    .sort();
}

/**
 * Sanitize date string to only allow YYYY-MM-DD format
 */
function sanitizeDate(date: string): string {
  if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
    throw new Error(`Invalid date format: ${date}. Expected YYYY-MM-DD`);
  }
  return date;
}

/**
 * Sanitize ID string to only allow alphanumeric and hyphens
 */
function sanitizeId(id: string): string {
  if (!/^[a-zA-Z0-9-]+$/.test(id)) {
    throw new Error(`Invalid ID format: ${id}. Only alphanumeric and hyphens allowed`);
  }
  return id;
}

/**
 * Verify that the resolved path is within the expected directory
 */
function verifyPathWithinDirectory(filepath: string, expectedDir: string): void {
  const resolvedPath = resolve(filepath);
  const resolvedDir = resolve(expectedDir);

  if (!resolvedPath.startsWith(resolvedDir + "/") && resolvedPath !== resolvedDir) {
    throw new Error(`Path traversal detected: ${filepath} is not within ${expectedDir}`);
  }
}

/**
 * Save an analytics batch to disk
 * Returns the filename that was created
 */
export function saveBatch(root: string, batch: AnalyticsBatch): string {
  ensureDirectories(root);

  const postsDir = join(root, "posts");

  // Sanitize inputs to prevent path traversal
  const date = sanitizeDate(batch.dateRange.from);
  const shortId = sanitizeId(batch.batchId.substring(0, 8));
  const filename = `${date}-${shortId}.json`;
  const filepath = join(postsDir, filename);

  // Verify the resolved filepath is within postsDir
  verifyPathWithinDirectory(filepath, postsDir);

  writeFileSync(filepath, JSON.stringify(batch, null, 2), "utf-8");

  return filename;
}

/**
 * Load all analytics batches from disk
 * Returns batches sorted by importedAt timestamp
 */
export function loadAllBatches(root: string): AnalyticsBatch[] {
  const postsDir = join(root, "posts");

  if (!existsSync(postsDir)) {
    return [];
  }

  const batches: AnalyticsBatch[] = [];

  for (const file of readdirSync(postsDir)) {
    if (!file.endsWith(".json")) {
      continue;
    }

    const filepath = join(postsDir, file);
    const content = readFileSync(filepath, "utf-8");
    try {
      const batch = JSON.parse(content) as AnalyticsBatch;
      batches.push(batch);
    } catch {
      // Skip corrupt batch file
      continue;
    }
  }

  return batches.sort((a, b) =>
    a.importedAt.localeCompare(b.importedAt)
  );
}

/**
 * Load all posts from all batches, deduplicated by post ID
 * Latest import wins. Sorted by publishedDate descending.
 */
export function loadAllPosts(root: string): PostAnalytics[] {
  const batches = loadAllBatches(root);

  // Use Map to deduplicate - key is post ID, value is { post, importedAt }
  const postMap = new Map<string, { post: PostAnalytics; importedAt: string }>();

  for (const batch of batches) {
    for (const post of batch.posts) {
      const existing = postMap.get(post.id);

      // Keep post with latest importedAt timestamp
      if (!existing || batch.importedAt > existing.importedAt) {
        postMap.set(post.id, {
          post,
          importedAt: batch.importedAt
        });
      }
    }
  }

  // Extract posts and sort by publishedDate descending
  const posts = Array.from(postMap.values()).map(({ post }) => post);

  return posts.sort((a, b) =>
    b.publishedDate.localeCompare(a.publishedDate)
  );
}

/**
 * Sanitize week string to only allow ISO week format (YYYY-WXX)
 */
function sanitizeWeek(week: string): string {
  if (!/^\d{4}-W\d{2}$/.test(week)) {
    throw new Error(`Invalid week format: ${week}. Expected YYYY-WXX`);
  }
  return week;
}

/**
 * Save a weekly report to disk
 * Returns the filename that was created
 */
export function saveWeeklyReport(root: string, report: WeeklyReport): string {
  ensureDirectories(root);

  const reportsDir = join(root, "weekly-reports");

  // Sanitize week to prevent path traversal
  const week = sanitizeWeek(report.week);
  const filename = `${week}.json`;
  const filepath = join(reportsDir, filename);

  // Verify the resolved filepath is within reportsDir
  verifyPathWithinDirectory(filepath, reportsDir);

  writeFileSync(filepath, JSON.stringify(report, null, 2), "utf-8");

  return filename;
}

/**
 * Load a specific weekly report by week identifier
 * Returns null if not found
 */
export function loadWeeklyReport(root: string, week: string): WeeklyReport | null {
  week = sanitizeWeek(week);
  const reportsDir = join(root, "weekly-reports");
  const filepath = join(reportsDir, `${week}.json`);

  if (!existsSync(filepath)) {
    return null;
  }

  const content = readFileSync(filepath, "utf-8");
  return JSON.parse(content) as WeeklyReport;
}

/**
 * Load all weekly reports from disk
 * Returns reports sorted by week descending (newest first)
 */
export function loadAllWeeklyReports(root: string): WeeklyReport[] {
  const reportsDir = join(root, "weekly-reports");

  if (!existsSync(reportsDir)) {
    return [];
  }

  const reports: WeeklyReport[] = [];

  for (const file of readdirSync(reportsDir)) {
    if (!file.endsWith(".json")) {
      continue;
    }

    const filepath = join(reportsDir, file);
    const content = readFileSync(filepath, "utf-8");
    const report = JSON.parse(content) as WeeklyReport;
    reports.push(report);
  }

  return reports.sort((a, b) =>
    b.week.localeCompare(a.week)
  );
}

/**
 * Sanitize month string to only allow YYYY-MM format
 */
function sanitizeMonth(month: string): string {
  if (!/^\d{4}-\d{2}$/.test(month)) {
    throw new Error(`Invalid month format: ${month}. Expected YYYY-MM`);
  }
  return month;
}

/**
 * Save a monthly report to disk
 */
export function saveMonthlyReport(root: string, report: MonthlyReport): string {
  ensureDirectories(root);
  const reportsDir = join(root, "monthly-reports");
  const month = sanitizeMonth(report.month);
  const filename = `${month}.json`;
  const filepath = join(reportsDir, filename);
  verifyPathWithinDirectory(filepath, reportsDir);
  writeFileSync(filepath, JSON.stringify(report, null, 2), "utf-8");
  return filename;
}

/**
 * Load a specific monthly report by month identifier
 */
export function loadMonthlyReport(root: string, month: string): MonthlyReport | null {
  month = sanitizeMonth(month);
  const reportsDir = join(root, "monthly-reports");
  const filepath = join(reportsDir, `${month}.json`);
  if (!existsSync(filepath)) return null;
  const content = readFileSync(filepath, "utf-8");
  return JSON.parse(content) as MonthlyReport;
}
