import fs from "node:fs/promises";
import path from "node:path";
import { pngBufferToBitmap } from "./pngBitmap.js";
import { createNativeScannerResultWorkflowService } from "./nativeScannerResultWorkflowService.js";
import { parseArtifactCandidate } from "../../src/lib/artifactOcrParser.js";
import { toStoredArtifact } from "../../src/lib/artifactStore.js";
import { matchParsedArtifactToIk, type IkArtifactCatalog } from "../../src/lib/ikArtifactMatcher.js";
import { createStoredScanResultEntry } from "../../src/lib/scanResultEntry.js";
import type {
  ArtifactStoreLoadResult,
  ArtifactStoreSaveResult,
  CaptureResult,
  NativeScannerImageLoadStatus,
  NativeScannerProcessStatus,
  NativeScannerPromotionStatus,
  NativeScannerReviewArtifactInput,
  NativeScannerReviewStatus,
  NativeScannerResultsLoadStatus,
  ReviewSamplePayload,
} from "../../src/types/global.js";
import type { ScanResultCategory, StoredArtifactRecord, StoredScanResultEntry } from "../../src/types/storage.js";

export type NativeCaptureJobPayload = {
  sequence: number;
  category?: string;
  page?: number;
  row?: number;
  col?: number;
  capturedAt?: string;
  relativePath?: string;
  absolutePath?: string;
};

export interface NativeScannerProcessingService {
  processRun(options?: { runDir?: string; persist?: boolean; limit?: number }): Promise<NativeScannerProcessStatus>;
  loadResults(options?: { runDir?: string; limit?: number }): Promise<NativeScannerResultsLoadStatus>;
  promoteResults(options: { runDir?: string; resultIds: string[] }): Promise<NativeScannerPromotionStatus>;
  reviewResult(options: {
    runDir?: string;
    resultId: string;
    action: "approve" | "reject";
    artifact?: NativeScannerReviewArtifactInput;
    note?: string;
  }): Promise<NativeScannerReviewStatus>;
  loadImage(options: { runDir?: string; imagePath: string }): Promise<NativeScannerImageLoadStatus>;
}

interface NativeScannerProcessingServiceDependencies {
  resolveRunDir(runDir?: string): string;
  buildCaptureResult(imagePath: string, job: NativeCaptureJobPayload): Promise<CaptureResult>;
  loadArtifacts?: () => Promise<ArtifactStoreLoadResult>;
  saveArtifacts(records: StoredArtifactRecord[]): Promise<Pick<ArtifactStoreSaveResult, "added" | "updated"> & Partial<ArtifactStoreSaveResult>>;
  saveReviewSample?: (sample: ReviewSamplePayload) => Promise<{ ok: boolean }>;
  loadIkArtifactCatalog?: () => Promise<IkArtifactCatalog | null>;
}

type ProcessedNativeJob = {
  result: NativeScannerProcessStatus["results"][number];
  scanResult: StoredScanResultEntry;
  storedRecord: StoredArtifactRecord | null;
};

const POST_CAPTURE_QUEUE_CONCURRENCY = 4;

export function createNativeScannerProcessingService(
  deps: NativeScannerProcessingServiceDependencies,
): NativeScannerProcessingService {
  const resultWorkflows = createNativeScannerResultWorkflowService(deps);
  return {
    async processRun(options = {}) {
      const started = Date.now();
      const runDir = deps.resolveRunDir(options.runDir);
      if (!runDir) {
        return emptyProcessStatus("No native scanner runDir available.");
      }

      const { jobsPath, jobs } = await readNativeCaptureJobs(runDir);
      const limit = Math.max(1, Math.min(jobs.length, Math.round(options.limit ?? jobs.length)));
      const selectedJobs = jobs.slice(0, limit);
      const runId = path.basename(runDir);
      const ikCatalog = deps.loadIkArtifactCatalog
        ? await deps.loadIkArtifactCatalog().catch(() => null)
        : null;

      const processedJobs = await mapWithConcurrency(
        selectedJobs,
        POST_CAPTURE_QUEUE_CONCURRENCY,
        (job) => processNativeCaptureJob({
          deps,
          ikCatalog,
          job,
          persist: Boolean(options.persist),
          runDir,
          runId,
        }),
      );
      const results = processedJobs.map((entry) => entry.result);
      const scanResults = processedJobs.map((entry) => entry.scanResult);
      const recordsToPersist = processedJobs
        .map((entry) => entry.storedRecord)
        .filter((record): record is StoredArtifactRecord => Boolean(record));

      let stored = 0;
      if (options.persist && recordsToPersist.length > 0) {
        const saved = await deps.saveArtifacts(recordsToPersist);
        stored = saved.added + saved.updated;
      }

      const status: NativeScannerProcessStatus = {
        ok: true,
        runDir,
        jobsPath,
        reportPath: path.join(runDir, "processing-report.json"),
        scanResultsPath: path.join(runDir, "scan-results.json"),
        processed: results.length,
        parsed: results.filter((result) => result.parsed).length,
        review: results.filter((result) => result.needsReview).length,
        stored,
        errors: results.filter((result) => result.error).length,
        elapsedMs: Date.now() - started,
        queueConcurrency: Math.min(POST_CAPTURE_QUEUE_CONCURRENCY, selectedJobs.length),
        persisted: Boolean(options.persist),
        results,
      };
      await fs.writeFile(status.scanResultsPath, JSON.stringify(scanResults, null, 2), "utf8");
      await fs.writeFile(status.reportPath, JSON.stringify(status, null, 2), "utf8");
      return status;
    },

    async loadResults(options = {}) {
      const runDir = deps.resolveRunDir(options.runDir);
      if (!runDir) {
        return emptyResultsStatus("No native scanner runDir available.");
      }

      const resultsPath = path.join(runDir, "scan-results.json");
      try {
        const raw = await fs.readFile(resultsPath, "utf8");
        const parsed = JSON.parse(raw);
        const results = Array.isArray(parsed)
          ? parsed.filter(isStoredScanResultEntry)
          : [];
        const limit = Number.isFinite(options.limit)
          ? Math.max(1, Math.min(results.length, Math.round(options.limit ?? results.length)))
          : results.length;
        return {
          ok: true,
          runDir,
          path: resultsPath,
          total: results.length,
          results: results.slice(-limit),
        };
      } catch (error) {
        return {
          ...emptyResultsStatus(error instanceof Error ? error.message : String(error)),
          runDir,
          path: resultsPath,
        };
      }
    },

    promoteResults: resultWorkflows.promoteResults,
    reviewResult: resultWorkflows.reviewResult,

    async loadImage(options) {
      const runDir = deps.resolveRunDir(options.runDir);
      if (!runDir) {
        return emptyImageStatus("No native scanner runDir available.");
      }

      const resolvedRunDir = path.resolve(runDir);
      const imagePath = path.isAbsolute(options.imagePath)
        ? path.resolve(options.imagePath)
        : path.resolve(resolvedRunDir, options.imagePath);
      if (!isPathInside(resolvedRunDir, imagePath)) {
        return { ...emptyImageStatus("Image path is outside the native scanner run directory."), runDir: resolvedRunDir, path: imagePath };
      }
      if (!/\.png$/i.test(imagePath)) {
        return { ...emptyImageStatus("Native scanner previews must be PNG files."), runDir: resolvedRunDir, path: imagePath };
      }

      try {
        const stat = await fs.stat(imagePath);
        if (stat.size > 20 * 1024 * 1024) {
          return { ...emptyImageStatus("Native scanner preview is too large."), runDir: resolvedRunDir, path: imagePath };
        }
        const buffer = await fs.readFile(imagePath);
        const bitmap = pngBufferToBitmap(buffer);
        return {
          ok: true,
          runDir: resolvedRunDir,
          path: imagePath,
          dataUrl: `data:image/png;base64,${buffer.toString("base64")}`,
          width: bitmap.width,
          height: bitmap.height,
        };
      } catch (error) {
        return {
          ...emptyImageStatus(error instanceof Error ? error.message : String(error)),
          runDir: resolvedRunDir,
          path: imagePath,
        };
      }
    },
  };
}

export function nativeScannerProcessStats(status: NativeScannerProcessStatus): Record<string, number> {
  return {
    processed: status.processed,
    parsed: status.parsed,
    review: status.review,
    stored: status.stored,
    errors: status.errors,
    elapsedMs: status.elapsedMs,
    queueConcurrency: status.queueConcurrency,
  };
}

async function processNativeCaptureJob({
  deps,
  ikCatalog,
  job,
  persist,
  runDir,
  runId,
}: {
  deps: NativeScannerProcessingServiceDependencies;
  ikCatalog: IkArtifactCatalog | null;
  job: NativeCaptureJobPayload;
  persist: boolean;
  runDir: string;
  runId: string;
}): Promise<ProcessedNativeJob> {
  const category = nativeJobCategory(job.category);
  const imagePath = job.absolutePath || (job.relativePath ? path.join(runDir, job.relativePath) : "");
  if (!category.artifactProcessingSupported) {
    return reviewJobResult({
      category: category.scanResultCategory,
      error: `Native post-capture processing for category '${category.nativeCategory}' is not implemented yet; IK catalog is available only.`,
      imagePath,
      job,
      runId,
    });
  }
  if (!imagePath || !(await fileExists(imagePath))) {
    const error = "Card crop image missing.";
    return reviewJobResult({ category: category.scanResultCategory, error, imagePath, job, runId });
  }

  try {
    const capture = await deps.buildCaptureResult(imagePath, job);
    const parsed = parseArtifactCandidate(capture);
    const ikMatch = parsed && ikCatalog ? matchParsedArtifactToIk(parsed, ikCatalog) : null;
    const notes = [...new Set([...(parsed?.notes ?? []), ...(ikMatch?.notes ?? [])])];
    const needsReview = parsedArtifactNeedsReview(parsed) || Boolean(ikMatch && !ikMatch.matched);
    const canPersist = parsedArtifactCanPersist(parsed, needsReview);
    const shouldPersist = Boolean(persist && parsed && canPersist && !needsReview);
    const storedRecord = parsed && shouldPersist
      ? toStoredArtifact(parsed, "native-ik-scan", needsReview, capture.locked)
      : null;
    return {
      result: {
        sequence: job.sequence,
        category: category.scanResultCategory,
        page: job.page,
        row: job.row,
        col: job.col,
        imagePath,
        parsed: Boolean(parsed),
        artifactName: parsed?.name,
        setName: parsed?.setName,
        slot: parsed?.slot,
        confidence: parsed?.confidence ?? 0,
        needsReview,
        persisted: shouldPersist,
        ikMatch: ikMatch ?? undefined,
        notes,
        ocr: capture.ocr ?? [],
      },
      scanResult: createStoredScanResultEntry({
        runId,
        sequence: job.sequence,
        page: job.page,
        row: job.row,
        col: job.col,
        category: category.scanResultCategory,
        source: "native-ik-scan",
        imagePath,
        parsed,
        needsReview,
        confidence: parsed?.confidence ?? 0,
        capturedAt: job.capturedAt,
        artifactRecordId: storedRecord?.id,
        persistedArtifact: shouldPersist,
        notes,
        locked: capture.locked,
        ikMatch: ikMatch ?? undefined,
      }),
      storedRecord,
    };
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    return reviewJobResult({ category: category.scanResultCategory, error: message, imagePath, job, runId });
  }
}

function reviewJobResult({
  category,
  error,
  imagePath,
  job,
  runId,
}: {
  category?: ScanResultCategory;
  error: string;
  imagePath: string;
  job: NativeCaptureJobPayload;
  runId: string;
}): ProcessedNativeJob {
  const scanResultCategory = category ?? nativeJobCategory(job.category).scanResultCategory;
  return {
    result: {
      sequence: job.sequence,
      category: scanResultCategory,
      page: job.page,
      row: job.row,
      col: job.col,
      imagePath,
      parsed: false,
      needsReview: true,
      confidence: 0,
      ocr: [],
      error,
    },
    scanResult: createStoredScanResultEntry({
      runId,
      sequence: job.sequence,
      page: job.page,
      row: job.row,
      col: job.col,
      category: scanResultCategory,
      source: "native-ik-scan",
      imagePath,
      parsed: null,
      needsReview: true,
      confidence: 0,
      capturedAt: job.capturedAt,
      error,
      notes: [error],
    }),
    storedRecord: null,
  };
}

function nativeJobCategory(category: string | undefined): {
  nativeCategory: string;
  scanResultCategory: ScanResultCategory;
  artifactProcessingSupported: boolean;
} {
  const nativeCategory = (category ?? "artifacts").trim().toLowerCase() || "artifacts";
  if (nativeCategory === "artifact" || nativeCategory === "artifacts") {
    return { nativeCategory, scanResultCategory: "artifact", artifactProcessingSupported: true };
  }
  if (nativeCategory === "weapon" || nativeCategory === "weapons") {
    return { nativeCategory, scanResultCategory: "weapon", artifactProcessingSupported: false };
  }
  if (nativeCategory === "character" || nativeCategory === "characters") {
    return { nativeCategory, scanResultCategory: "character", artifactProcessingSupported: false };
  }
  if (nativeCategory === "material" || nativeCategory === "materials") {
    return { nativeCategory, scanResultCategory: "material", artifactProcessingSupported: false };
  }
  return { nativeCategory, scanResultCategory: "unknown", artifactProcessingSupported: false };
}

async function mapWithConcurrency<TInput, TOutput>(
  items: readonly TInput[],
  concurrency: number,
  worker: (item: TInput, index: number) => Promise<TOutput>,
) {
  const output = Array<TOutput>(items.length);
  let nextIndex = 0;
  const workerCount = Math.max(1, Math.min(items.length, Math.floor(concurrency)));
  await Promise.all(Array.from({ length: workerCount }, async () => {
    while (nextIndex < items.length) {
      const index = nextIndex++;
      output[index] = await worker(items[index], index);
    }
  }));
  return output;
}

async function readNativeCaptureJobs(runDir: string): Promise<{ jobsPath: string; jobs: NativeCaptureJobPayload[] }> {
  const jobsPath = path.join(runDir, "capture-jobs.jsonl");
  const raw = await fs.readFile(jobsPath, "utf8");
  const jobs = raw
    .split(/\r?\n/)
    .map((line) => line.trim())
    .filter(Boolean)
    .map((line) => JSON.parse(line) as NativeCaptureJobPayload)
    .filter((job) => Number.isFinite(job.sequence));
  return { jobsPath, jobs };
}

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

function emptyProcessStatus(error: string): NativeScannerProcessStatus {
  return {
    ok: false,
    runDir: "",
    jobsPath: "",
    reportPath: "",
    scanResultsPath: "",
    processed: 0,
    parsed: 0,
    review: 0,
    stored: 0,
    errors: 1,
    elapsedMs: 0,
    queueConcurrency: 0,
    persisted: false,
    results: [],
    error,
  };
}

function emptyResultsStatus(error: string): NativeScannerResultsLoadStatus {
  return {
    ok: false,
    runDir: "",
    path: "",
    total: 0,
    results: [],
    error,
  };
}

function emptyImageStatus(error: string): NativeScannerImageLoadStatus {
  return {
    ok: false,
    runDir: "",
    path: "",
    dataUrl: "",
    width: 0,
    height: 0,
    error,
  };
}

function isPathInside(root: string, candidate: string) {
  const relative = path.relative(root, candidate);
  return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}

function isStoredScanResultEntry(value: unknown): value is StoredScanResultEntry {
  if (!value || typeof value !== "object") return false;
  const entry = value as Partial<StoredScanResultEntry>;
  return typeof entry.id === "string"
    && typeof entry.runId === "string"
    && Number.isFinite(entry.sequence)
    && typeof entry.source === "string"
    && typeof entry.imagePath === "string"
    && typeof entry.extractionStatus === "string"
    && typeof entry.valueStatus === "string"
    && Array.isArray(entry.notes);
}

function parsedArtifactNeedsReview(parsed: ReturnType<typeof parseArtifactCandidate>) {
  if (!parsed) return true;
  if (parsed.confidence < 78) return true;
  const criticalFields: Array<"name" | "slot" | "mainStat" | "mainValue" | "setName"> = ["name", "slot", "mainStat", "mainValue", "setName"];
  if (criticalFields.some((field) => (parsed.fields[field]?.confidence ?? 0) < 70)) return true;
  if (parsed.substats.length === 0) return true;
  return parsed.notes.some((note) => /not confidently parsed|substats look incomplete|likely OCR misread/i.test(note));
}

function parsedArtifactCanPersist(parsed: ReturnType<typeof parseArtifactCandidate>, needsReview: boolean) {
  if (!parsed) return false;
  if (parsed.name === "Unknown artifact") return false;
  if (parsed.slot === "Unknown slot") return false;
  if (parsed.setName === "Unknown set") return false;
  if (parsed.mainStat === "Unknown main stat") return false;
  if (parsed.mainValue === "?") return false;
  if (parsed.substats.length === 0) return false;
  if (!needsReview && parsed.confidence < 68) return false;
  if (needsReview && parsed.confidence < 60) return false;
  return true;
}
