import type { CaptureResult, RuntimeInfo } from "../types/global";

export type GridTarget = { x: number; y: number; row: number; col: number };

export type GridModel = {
  targets: GridTarget[];
  cols: number;
  rows: number;
  startX: number;
  startY: number;
  stepX: number;
  stepY: number;
  anchorX: number;
  anchorY: number;
  confidence: number;
  source: "detected" | "fallback" | "missing";
};

export type InventoryPagePlan = {
  pageTargets: GridTarget[];
  remainingTargets: number;
  totalRows: number;
  remainingRows: number;
  startIndex: number;
  rowsQueuedAfterPage: number;
  scrollRowsAfterPage: number;
};

// Matches Inventory Kamera's model: the whole app simply always runs
// elevated (see docs/DECISIONS.md ADR-007), so auto-scan only needs to check
// our own process elevation - no per-process target-elevation probing or
// separate broker process required.
export function requiresAdminForAutomation(runtime: RuntimeInfo | null | undefined) {
  return runtime?.ok === true && runtime.isElevated !== true;
}

export function automationBlockReason(runtime: RuntimeInfo | null | undefined) {
  if (!requiresAdminForAutomation(runtime)) return "";
  return "Auto-Scan braucht Administrator-Rechte, damit Windows die simulierten Eingaben an Genshin nicht blockiert.";
}

export function buildGridModel(grid: CaptureResult["inventoryGrid"] | undefined): GridModel | null {
  if (!grid || grid.source === "missing" || grid.centers.length === 0) return null;

  const centers = grid.centers
    .filter((center) => Number.isFinite(center.x) && Number.isFinite(center.y))
    .map((center) => ({
      x: Math.round(center.x),
      y: Math.round(center.y),
      row: Math.max(0, Math.round(center.row)),
      col: Math.max(0, Math.round(center.col)),
    }));

  if (centers.length === 0) return null;

  const cols = Math.max(1, grid.cols || Math.max(...centers.map((center) => center.col)) + 1);
  const rows = Math.max(1, grid.rows || Math.max(...centers.map((center) => center.row)) + 1);
  const xCoords = buildAxisCoordinates(centers, "col", "x", cols);
  const yCoords = buildAxisCoordinates(centers, "row", "y", rows);

  if (xCoords.length !== cols || yCoords.length !== rows) return null;

  const targets = yCoords.flatMap((y, row) =>
    xCoords.map((x, col) => ({
      x: Math.round(x),
      y: Math.round(y),
      row,
      col,
    })),
  );

  const stepX = Math.max(1, Math.round(medianDiff(xCoords) || 1));
  const stepY = Math.max(1, Math.round(medianDiff(yCoords) || 1));

  return {
    targets,
    cols,
    rows,
    startX: targets[0]?.x ?? Math.round(xCoords[0]),
    startY: targets[0]?.y ?? Math.round(yCoords[0]),
    stepX,
    stepY,
    anchorX: Math.round(xCoords[Math.min(1, xCoords.length - 1)] ?? xCoords[0]),
    anchorY: Math.round(yCoords[Math.floor(yCoords.length / 2)] ?? yCoords[0]),
    confidence: grid.confidence,
    source: grid.source,
  };
}

export function buildInventoryPagePlan({
  targets,
  cols,
  rows,
  totalTargetCount,
  processedTargets,
  rowsQueued,
}: {
  targets: GridTarget[];
  cols: number;
  rows: number;
  totalTargetCount: number;
  processedTargets: number;
  rowsQueued: number;
}): InventoryPagePlan {
  const safeCols = Math.max(1, cols);
  const safeRows = Math.max(1, rows);
  const safeTotal = Math.max(0, totalTargetCount);
  const safeProcessed = Math.max(0, Math.min(safeTotal, processedTargets));
  const remainingTargets = Math.max(0, safeTotal - safeProcessed);
  const totalRows = Math.max(0, Math.ceil(safeTotal / safeCols));
  const remainingRows = Math.max(0, totalRows - rowsQueued);
  const fullPage = safeCols * safeRows;
  const pageTargetCount = Math.min(remainingTargets, fullPage);
  const partialPage = pageTargetCount > 0 && pageTargetCount < fullPage;
  // Only bottom-align a partial page after we have already consumed at least
  // one full visible page. The first page of a small inventory starts at the
  // top of the grid, while the final scrolled page is bottom-aligned because
  // it overlaps previous rows.
  const alignPartialPageToBottom = rowsQueued > 0 && partialPage && remainingRows > 0 && remainingRows < safeRows;
  const startIndex = alignPartialPageToBottom ? Math.max(0, (safeRows - remainingRows) * safeCols) : 0;
  const pageTargets = targets.slice(startIndex, Math.min(targets.length, startIndex + pageTargetCount));
  const rowsQueuedAfterPage = Math.min(totalRows, rowsQueued + safeRows);
  const remainingRowsAfterPage = Math.max(0, totalRows - rowsQueuedAfterPage);
  const scrollRowsAfterPage = remainingRowsAfterPage > 0 ? Math.min(safeRows, remainingRowsAfterPage) : 0;

  return {
    pageTargets,
    remainingTargets,
    totalRows,
    remainingRows,
    startIndex,
    rowsQueuedAfterPage,
    scrollRowsAfterPage,
  };
}

function buildAxisCoordinates(
  centers: GridTarget[],
  groupKey: "row" | "col",
  valueKey: "x" | "y",
  expectedCount: number,
) {
  const known = Array.from({ length: expectedCount }, (_, index) => {
    const values = centers
      .filter((center) => center[groupKey] === index)
      .map((center) => center[valueKey]);
    return values.length > 0 ? median(values) : null;
  });

  const knownPairs = known
    .map((value, index) => ({ index, value }))
    .filter((entry): entry is { index: number; value: number } => typeof entry.value === "number");

  if (knownPairs.length === expectedCount) return known as number[];
  if (knownPairs.length === 0) return [];

  const indexedSteps = knownPairs
    .slice(1)
    .map((entry, index) => {
      const previous = knownPairs[index];
      const indexDiff = entry.index - previous.index;
      return indexDiff > 0 ? Math.abs(entry.value - previous.value) / indexDiff : 0;
    })
    .filter((step) => step > 3);
  const step = median(indexedSteps) || estimateStepFromAllCenters(centers.map((center) => center[valueKey]));
  if (!step || !Number.isFinite(step)) return [];

  const first = knownPairs[0];
  const start = first.value - step * first.index;
  return Array.from({ length: expectedCount }, (_, index) => Math.round(start + step * index));
}

function estimateStepFromAllCenters(values: number[]) {
  const sorted = [...new Set(values.map((value) => Math.round(value)))].sort((a, b) => a - b);
  return medianDiff(sorted);
}

function median(values: number[]) {
  if (values.length === 0) return 0;
  const sorted = [...values].sort((a, b) => a - b);
  const mid = Math.floor(sorted.length / 2);
  return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
}

function medianDiff(values: number[]) {
  if (values.length < 2) return 0;
  const sorted = [...values].sort((a, b) => a - b);
  const diffs = sorted
    .slice(1)
    .map((value, index) => Math.abs(value - sorted[index]))
    .filter((diff) => diff > 3);
  return diffs.length > 0 ? median(diffs) : 0;
}
