import { env } from "../../config/env";
import fs from "node:fs/promises";
import path from "node:path";
import { ApiError } from "../errors";
import {
  claimNextPendingExportJob,
  completeExportJob,
  createExportJob,
  failExportJob,
  markExpiredExportJobs,
  resetStaleRunningExportJobs,
  type ExportJobEntity
} from "../store/exportJobStore";
import { listSettlementAdjustments, settlementAdjustmentReportToCsv } from "../store/settlementStore";
import { logger } from "./logger";

export type SettlementAdjustmentExportRequest = {
  merchant_id?: string;
  adjustment_type?: string;
  approval_status?: string;
  from?: string;
  to?: string;
  limit?: number;
};

type WorkerStatus = {
  enabled: boolean;
  running: boolean;
  interval_ms: number;
  batch_size: number;
  max_settlement_adjustment_rows: number;
  storage_dir: string;
  retention_days: number;
  last_started_at: string | null;
  last_finished_at: string | null;
  last_error: string | null;
  last_processed_job_id: string | null;
  last_processed_count: number;
  stale_reset_count: number;
  expired_cleanup_count: number;
};

const enabled = String(env.EXPORT_WORKER_ENABLED).toLowerCase() !== "false";
const intervalMs = Math.max(env.EXPORT_WORKER_INTERVAL_MS || 2000, 500);
const batchSize = Math.min(Math.max(env.EXPORT_WORKER_BATCH_SIZE || 2, 1), 10);
const staleRunningMs = Math.max(env.EXPORT_JOB_STALE_RUNNING_MS || 900000, 60000);
const maxSettlementAdjustmentRows = Math.min(Math.max(env.EXPORT_SETTLEMENT_ADJUSTMENT_MAX_ROWS || 5000, 1), 10000);
const exportStorageDir = path.resolve(process.cwd(), env.EXPORT_STORAGE_DIR || "./storage/exports");
const retentionDays = Math.max(env.EXPORT_RETENTION_DAYS || 7, 1);

let timer: NodeJS.Timeout | null = null;
let running = false;
let lastStartedAt: string | null = null;
let lastFinishedAt: string | null = null;
let lastError: string | null = null;
let lastProcessedJobId: string | null = null;
let lastProcessedCount = 0;
let staleResetCount = 0;
let expiredCleanupCount = 0;
let staleResetDone = false;

function exportExpiresAt() {
  return new Date(Date.now() + retentionDays * 24 * 60 * 60 * 1000).toISOString();
}

function safeFilename(value: string) {
  return value.replace(/[^a-zA-Z0-9._-]/g, "-");
}

async function writeExportResult(job: ExportJobEntity, filename: string, body: string) {
  await fs.mkdir(exportStorageDir, { recursive: true });
  const safeName = safeFilename(filename);
  const filePath = path.join(exportStorageDir, `${job.id}-${safeName}`);
  await fs.writeFile(filePath, body, "utf8");
  return {
    path: filePath,
    size: Buffer.byteLength(body, "utf8")
  };
}

function parseSettlementAdjustmentType(value?: string): "credit" | "debit" | undefined {
  if (!value) {
    return undefined;
  }
  return value === "credit" || value === "debit" ? value : undefined;
}

function parseSettlementAdjustmentApprovalStatus(value?: string): "pending" | "approved" | "rejected" | undefined {
  if (!value) {
    return undefined;
  }
  return value === "pending" || value === "approved" || value === "rejected" ? value : undefined;
}

function isIsoDate(value: string) {
  const date = new Date(value);
  return !Number.isNaN(date.getTime());
}

function normalizeSettlementAdjustmentExportRequest(payload: SettlementAdjustmentExportRequest) {
  const merchantId = payload.merchant_id?.trim();
  const adjustmentType = parseSettlementAdjustmentType(payload.adjustment_type);
  const approvalStatus = parseSettlementAdjustmentApprovalStatus(payload.approval_status);
  const from = payload.from?.trim();
  const to = payload.to?.trim();
  const limit = payload.limit ? Number(payload.limit) : 500;

  if (payload.adjustment_type && !adjustmentType) {
    throw new ApiError("BAD_REQUEST", "adjustment_type must be credit|debit", 400);
  }
  if (payload.approval_status && !approvalStatus) {
    throw new ApiError("BAD_REQUEST", "approval_status must be pending|approved|rejected", 400);
  }
  if (from && !isIsoDate(from)) {
    throw new ApiError("BAD_REQUEST", "from must be valid ISO datetime", 400);
  }
  if (to && !isIsoDate(to)) {
    throw new ApiError("BAD_REQUEST", "to must be valid ISO datetime", 400);
  }
  if (!Number.isFinite(limit) || limit <= 0) {
    throw new ApiError("BAD_REQUEST", "limit must be a positive number", 400);
  }

  return {
    merchant_id: merchantId || undefined,
    adjustment_type: adjustmentType,
    approval_status: approvalStatus,
    from: from || undefined,
    to: to || undefined,
    limit: Math.min(limit, maxSettlementAdjustmentRows),
    max_limit: maxSettlementAdjustmentRows
  };
}

export async function createSettlementAdjustmentExportJob(payload: {
  requested_by?: string;
  request: SettlementAdjustmentExportRequest;
}) {
  const requestJson = normalizeSettlementAdjustmentExportRequest(payload.request);
  return createExportJob({
    job_type: "settlement_adjustments_csv",
    requested_by: payload.requested_by || "admin",
    request_json: requestJson
  });
}

async function processSettlementAdjustmentExport(job: ExportJobEntity) {
  const report = await listSettlementAdjustments({
    ...(job.request_json || {}),
    max_limit: maxSettlementAdjustmentRows
  });
  const csv = settlementAdjustmentReportToCsv(report);
  const filename = `settlement-adjustment-report-${job.id}.csv`;
  const stored = await writeExportResult(job, filename, csv);
  return completeExportJob(job.id, {
    result_content_type: "text/csv; charset=utf-8",
    result_filename: filename,
    result_storage_path: stored.path,
    result_size_bytes: stored.size,
    expires_at: exportExpiresAt()
  });
}

async function processExportJob(job: ExportJobEntity) {
  if (job.job_type === "settlement_adjustments_csv") {
    return processSettlementAdjustmentExport(job);
  }
  throw new Error(`unsupported export job type: ${job.job_type}`);
}

async function runWorkerOnce() {
  if (running) {
    return;
  }

  running = true;
  lastStartedAt = new Date().toISOString();
  lastError = null;
  lastProcessedCount = 0;

  try {
    if (!staleResetDone) {
      await resetStaleRunningJobs();
      await cleanupExpiredExports();
      staleResetDone = true;
    }

    for (let i = 0; i < batchSize; i += 1) {
      const job = await claimNextPendingExportJob();
      if (!job) {
        break;
      }

      try {
        await processExportJob(job);
        lastProcessedJobId = job.id;
        lastProcessedCount += 1;
        logger.info("export_job_completed", { job_id: job.id, job_type: job.job_type });
      } catch (error) {
        const message = error instanceof Error ? error.message : "unknown";
        await failExportJob(job.id, message);
        lastError = message;
        logger.error("export_job_failed", { job_id: job.id, job_type: job.job_type, error });
      }
    }
  } catch (error) {
    lastError = error instanceof Error ? error.message : "unknown";
    logger.error("export_worker_run_failed", { error });
  } finally {
    running = false;
    lastFinishedAt = new Date().toISOString();
  }
}

async function resetStaleRunningJobs() {
  const cutoffIso = new Date(Date.now() - staleRunningMs).toISOString();
  const resetCount = await resetStaleRunningExportJobs(cutoffIso);
  staleResetCount += resetCount;
  if (resetCount > 0) {
    logger.warn("export_jobs_reset_stale_running", { reset_count: resetCount, cutoff_at: cutoffIso });
  }
}

async function cleanupExpiredExports() {
  const expired = await markExpiredExportJobs(new Date().toISOString());
  for (const job of expired) {
    if (!job.result_storage_path) {
      continue;
    }
    try {
      await fs.unlink(job.result_storage_path);
    } catch (error) {
      const code = (error as NodeJS.ErrnoException).code;
      if (code !== "ENOENT") {
        logger.warn("export_result_file_delete_failed", {
          job_id: job.id,
          path: job.result_storage_path,
          error
        });
      }
    }
  }
  expiredCleanupCount += expired.length;
  if (expired.length > 0) {
    logger.info("export_results_expired", { count: expired.length });
  }
}

export function startExportJobWorker() {
  if (!enabled || timer) {
    return;
  }

  timer = setInterval(() => {
    void runWorkerOnce();
  }, intervalMs);
  timer.unref?.();
}

export function getExportJobWorkerStatus(): WorkerStatus {
  return {
    enabled,
    running,
    interval_ms: intervalMs,
    batch_size: batchSize,
    max_settlement_adjustment_rows: maxSettlementAdjustmentRows,
    storage_dir: exportStorageDir,
    retention_days: retentionDays,
    last_started_at: lastStartedAt,
    last_finished_at: lastFinishedAt,
    last_error: lastError,
    last_processed_job_id: lastProcessedJobId,
    last_processed_count: lastProcessedCount,
    stale_reset_count: staleResetCount,
    expired_cleanup_count: expiredCleanupCount
  };
}
