export type GameBoard = number[][];
export type Difficulty = 'easy' | 'medium' | 'hard';
export type GameMode = 'standard' | 'irregular';
export type RegionMap = number[][];
export type GeneratedPuzzle = {
  puzzle: GameBoard;
  regionMap: RegionMap;
};

type SolvedSudoku = {
  fullBoard: GameBoard;
  regionMap: RegionMap;
};

type CellPosition = [number, number];

const DIGITS = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const DIRECTIONS: CellPosition[] = [
  [-1, 0],
  [1, 0],
  [0, -1],
  [0, 1],
];

const MIN_IRREGULAR_CELL_DIFFERENCE = 16;
const MAX_IRREGULAR_RESTARTS = 24;
const MAX_IRREGULAR_GREEDY_STEPS = 28;

const generatedFullBoards = new WeakMap<RegionMap, GameBoard>();

export const createEmptyBoard = (): GameBoard => Array.from({ length: 9 }, () => Array(9).fill(0));

const shuffleArray = <T>(arr: T[]): T[] => {
  const result = [...arr];
  for (let i = result.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [result[i], result[j]] = [result[j], result[i]];
  }
  return result;
};

const cloneBoard = (board: GameBoard): GameBoard => board.map((row) => [...row]);

const cloneRegionMap = (regionMap: RegionMap): RegionMap => regionMap.map((row) => [...row]);

export const STANDARD_REGION_MAP: RegionMap = Array.from({ length: 9 }, (_, row) =>
  Array.from({ length: 9 }, (_, col) => Math.floor(row / 3) * 3 + Math.floor(col / 3))
);

export const IRREGULAR_REGION_MAP: RegionMap = [
  [0, 0, 0, 0, 0, 1, 1, 1, 1],
  [2, 2, 0, 0, 3, 3, 1, 1, 1],
  [2, 2, 0, 0, 3, 1, 1, 4, 4],
  [2, 3, 3, 3, 3, 4, 4, 4, 4],
  [2, 2, 2, 2, 3, 4, 5, 5, 5],
  [6, 6, 6, 6, 3, 4, 7, 5, 5],
  [6, 6, 6, 8, 8, 4, 7, 5, 5],
  [6, 6, 8, 8, 8, 7, 7, 7, 5],
  [8, 8, 8, 8, 7, 7, 7, 7, 5],
];

const IRREGULAR_FULL_BOARD: GameBoard = [
  [2, 3, 6, 9, 4, 7, 5, 8, 1],
  [5, 2, 7, 8, 6, 1, 3, 9, 4],
  [3, 4, 1, 5, 9, 6, 2, 7, 8],
  [8, 5, 2, 4, 7, 9, 1, 3, 6],
  [6, 1, 9, 7, 3, 5, 8, 4, 2],
  [1, 7, 5, 2, 8, 4, 9, 6, 3],
  [9, 8, 3, 6, 5, 2, 4, 1, 7],
  [4, 6, 8, 1, 2, 3, 7, 5, 9],
  [7, 9, 4, 3, 1, 8, 6, 2, 5],
];

export const getRegionId = (regionMap: RegionMap, row: number, col: number): number => regionMap[row][col];

const isInBounds = (row: number, col: number): boolean => row >= 0 && row < 9 && col >= 0 && col < 9;

export const isValidRegionMap = (regionMap: RegionMap): boolean => {
  if (regionMap.length !== 9 || regionMap.some((row) => row.length !== 9)) {
    return false;
  }

  const cellsByRegion = Array.from({ length: 9 }, () => [] as Array<[number, number]>);

  for (let row = 0; row < 9; row++) {
    for (let col = 0; col < 9; col++) {
      const regionId = regionMap[row][col];
      if (!Number.isInteger(regionId) || regionId < 0 || regionId > 8) {
        return false;
      }
      cellsByRegion[regionId].push([row, col]);
    }
  }

  return cellsByRegion.every((cells) => cells.length === 9 && isConnectedRegion(cells));
};

const isConnectedRegion = (cells: Array<[number, number]>): boolean => {
  if (cells.length === 0) return false;

  const cellSet = new Set(cells.map(([row, col]) => `${row}-${col}`));
  const seen = new Set<string>();
  const queue: CellPosition[] = [cells[0]];
  seen.add(`${cells[0][0]}-${cells[0][1]}`);

  for (let index = 0; index < queue.length; index++) {
    const [row, col] = queue[index];
    for (const [rowOffset, colOffset] of DIRECTIONS) {
      const nextRow = row + rowOffset;
      const nextCol = col + colOffset;
      const key = `${nextRow}-${nextCol}`;
      if (cellSet.has(key) && !seen.has(key)) {
        seen.add(key);
        queue.push([nextRow, nextCol]);
      }
    }
  }

  return seen.size === cells.length;
};

const isSameRegionMap = (left: RegionMap, right: RegionMap): boolean =>
  left.length === right.length &&
  left.every((row, rowIndex) => row.length === right[rowIndex].length && row.every((cell, colIndex) => cell === right[rowIndex][colIndex]));

const randomizeDigits = (board: GameBoard): GameBoard => {
  const shuffledDigits = shuffleArray(DIGITS);
  const digitMap = new Map(shuffledDigits.map((digit, index) => [index + 1, digit]));

  return board.map((row) => row.map((cell) => digitMap.get(cell) ?? cell));
};

const generateRandomStandardFullBoard = (): GameBoard => {
  const pattern = (row: number, col: number) => ((row * 3 + Math.floor(row / 3) + col) % 9) + 1;
  const rows = shuffleArray([0, 1, 2]).flatMap((band) => shuffleArray([0, 1, 2]).map((row) => band * 3 + row));
  const cols = shuffleArray([0, 1, 2]).flatMap((stack) => shuffleArray([0, 1, 2]).map((col) => stack * 3 + col));
  const shuffledDigits = shuffleArray(DIGITS);

  return rows.map((row) => cols.map((col) => shuffledDigits[pattern(row, col) - 1]));
};

const countDifferentCells = (left: RegionMap, right: RegionMap): number => {
  let count = 0;
  for (let row = 0; row < 9; row++) {
    for (let col = 0; col < 9; col++) {
      if (left[row][col] !== right[row][col]) {
        count++;
      }
    }
  }
  return count;
};

const getTouchingRegionPairs = (regionMap: RegionMap): Array<[number, number]> => {
  const pairs = new Set<string>();

  for (let row = 0; row < 9; row++) {
    for (let col = 0; col < 9; col++) {
      for (const [rowOffset, colOffset] of DIRECTIONS) {
        const nextRow = row + rowOffset;
        const nextCol = col + colOffset;
        if (!isInBounds(nextRow, nextCol)) continue;

        const currentRegion = regionMap[row][col];
        const nextRegion = regionMap[nextRow][nextCol];
        if (currentRegion === nextRegion) continue;

        const [low, high] = currentRegion < nextRegion ? [currentRegion, nextRegion] : [nextRegion, currentRegion];
        pairs.add(`${low}-${high}`);
      }
    }
  }

  return Array.from(pairs, (pair) => pair.split('-').map(Number) as [number, number]);
};

const getBoundaryCells = (regionMap: RegionMap, regionId: number, neighborRegionId: number): CellPosition[] => {
  const cells: CellPosition[] = [];

  for (let row = 0; row < 9; row++) {
    for (let col = 0; col < 9; col++) {
      if (regionMap[row][col] !== regionId) continue;

      const touchesNeighbor = DIRECTIONS.some(([rowOffset, colOffset]) => {
        const nextRow = row + rowOffset;
        const nextCol = col + colOffset;
        return isInBounds(nextRow, nextCol) && regionMap[nextRow][nextCol] === neighborRegionId;
      });

      if (touchesNeighbor) {
        cells.push([row, col]);
      }
    }
  }

  return cells;
};

const swapRegionCells = (
  regionMap: RegionMap,
  firstCell: CellPosition,
  secondCell: CellPosition,
  firstRegionId: number,
  secondRegionId: number
): RegionMap => {
  const nextRegionMap = cloneRegionMap(regionMap);
  const [firstRow, firstCol] = firstCell;
  const [secondRow, secondCol] = secondCell;

  nextRegionMap[firstRow][firstCol] = secondRegionId;
  nextRegionMap[secondRow][secondCol] = firstRegionId;

  return nextRegionMap;
};

const findBetterCompatibleRegionMap = (
  regionMap: RegionMap,
  fullBoard: GameBoard,
  currentDifference: number
): RegionMap | null => {
  let bestDifference = currentDifference;
  let bestRegionMaps: RegionMap[] = [];

  for (const [firstRegionId, secondRegionId] of shuffleArray(getTouchingRegionPairs(regionMap))) {
    const firstBoundaryCells = shuffleArray(getBoundaryCells(regionMap, firstRegionId, secondRegionId));
    const secondBoundaryCells = getBoundaryCells(regionMap, secondRegionId, firstRegionId);

    for (const firstCell of firstBoundaryCells) {
      const [firstRow, firstCol] = firstCell;
      const matchingSecondCells = secondBoundaryCells.filter(([secondRow, secondCol]) => fullBoard[secondRow][secondCol] === fullBoard[firstRow][firstCol]);

      for (const secondCell of shuffleArray(matchingSecondCells)) {
        const candidateRegionMap = swapRegionCells(regionMap, firstCell, secondCell, firstRegionId, secondRegionId);
        const candidateDifference = countDifferentCells(candidateRegionMap, STANDARD_REGION_MAP);

        if (candidateDifference <= bestDifference || !isValidRegionMap(candidateRegionMap)) {
          continue;
        }

        if (candidateDifference > bestDifference) {
          bestDifference = candidateDifference;
          bestRegionMaps = [];
        }

        bestRegionMaps.push(candidateRegionMap);
      }
    }
  }

  if (bestRegionMaps.length === 0) {
    return null;
  }

  return shuffleArray(bestRegionMaps)[0];
};

const isSolvedBoardForRegionMap = (board: GameBoard, regionMap: RegionMap): boolean => {
  const expected = DIGITS.join(',');
  const toKey = (values: number[]) => [...values].sort((a, b) => a - b).join(',');

  for (let index = 0; index < 9; index++) {
    if (toKey(board[index]) !== expected) return false;
    if (toKey(board.map((row) => row[index])) !== expected) return false;

    const regionValues: number[] = [];
    for (let row = 0; row < 9; row++) {
      for (let col = 0; col < 9; col++) {
        if (regionMap[row][col] === index) {
          regionValues.push(board[row][col]);
        }
      }
    }

    if (toKey(regionValues) !== expected) return false;
  }

  return true;
};

const rememberSolvedSudoku = (solvedSudoku: SolvedSudoku): SolvedSudoku => {
  generatedFullBoards.set(solvedSudoku.regionMap, cloneBoard(solvedSudoku.fullBoard));
  return solvedSudoku;
};

const generateSolvedIrregularSudoku = (): SolvedSudoku => {
  let bestSolvedSudoku: SolvedSudoku | null = null;
  let bestDifference = -1;

  for (let restart = 0; restart < MAX_IRREGULAR_RESTARTS; restart++) {
    const fullBoard = generateRandomStandardFullBoard();
    let regionMap = cloneRegionMap(STANDARD_REGION_MAP);
    let difference = 0;

    for (let step = 0; step < MAX_IRREGULAR_GREEDY_STEPS; step++) {
      const nextRegionMap = findBetterCompatibleRegionMap(regionMap, fullBoard, difference);
      if (!nextRegionMap) break;

      regionMap = nextRegionMap;
      difference = countDifferentCells(regionMap, STANDARD_REGION_MAP);
    }

    if (isValidRegionMap(regionMap) && isSolvedBoardForRegionMap(fullBoard, regionMap) && difference > bestDifference) {
      bestSolvedSudoku = { fullBoard, regionMap };
      bestDifference = difference;
    }

    if (bestSolvedSudoku && bestDifference >= MIN_IRREGULAR_CELL_DIFFERENCE) {
      return rememberSolvedSudoku(bestSolvedSudoku);
    }
  }

  if (bestSolvedSudoku && bestDifference > 0) {
    return rememberSolvedSudoku(bestSolvedSudoku);
  }

  return rememberSolvedSudoku({
    fullBoard: randomizeDigits(IRREGULAR_FULL_BOARD),
    regionMap: cloneRegionMap(IRREGULAR_REGION_MAP),
  });
};

export const generateIrregularRegionMap = (): RegionMap => generateSolvedIrregularSudoku().regionMap;

export const isPlacementValid = (
  board: GameBoard,
  row: number,
  col: number,
  num: number,
  regionMap: RegionMap
): boolean => {
  for (let c = 0; c < 9; c++) {
    if (c !== col && board[row][c] === num) return false;
  }

  for (let r = 0; r < 9; r++) {
    if (r !== row && board[r][col] === num) return false;
  }

  const targetRegion = getRegionId(regionMap, row, col);
  for (let r = 0; r < 9; r++) {
    for (let c = 0; c < 9; c++) {
      if ((r !== row || c !== col) && getRegionId(regionMap, r, c) === targetRegion && board[r][c] === num) {
        return false;
      }
    }
  }

  return true;
};

const fillBoard = (board: GameBoard, regionMap: RegionMap): boolean => {
  let bestCell: { row: number; col: number; candidates: number[] } | null = null;

  for (let row = 0; row < 9; row++) {
    for (let col = 0; col < 9; col++) {
      if (board[row][col] !== 0) continue;

      const candidates = DIGITS.filter((num) => isPlacementValid(board, row, col, num, regionMap));
      if (!bestCell || candidates.length < bestCell.candidates.length) {
        bestCell = { row, col, candidates };
      }

      if (candidates.length <= 1) {
        break;
      }
    }

    if (bestCell && bestCell.candidates.length <= 1) {
      break;
    }
  }

  if (!bestCell) {
    return true;
  }

  for (const num of shuffleArray(bestCell.candidates)) {
    board[bestCell.row][bestCell.col] = num;
    if (fillBoard(board, regionMap)) {
      return true;
    }
    board[bestCell.row][bestCell.col] = 0;
  }

  return false;
};

export const generateFullBoard = (regionMap: RegionMap): GameBoard => {
  if (!isValidRegionMap(regionMap)) {
    throw new Error('Invalid sudoku region map');
  }

  const generatedFullBoard = generatedFullBoards.get(regionMap);
  if (generatedFullBoard) {
    return cloneBoard(generatedFullBoard);
  }

  if (isSameRegionMap(regionMap, IRREGULAR_REGION_MAP)) {
    return randomizeDigits(IRREGULAR_FULL_BOARD);
  }

  const board = createEmptyBoard();
  if (!fillBoard(board, regionMap)) {
    throw new Error('Unable to generate sudoku board');
  }
  return board;
};

const getRemovalCount = (difficulty: Difficulty) => {
  switch (difficulty) {
    case 'easy':
      return 35;
    case 'medium':
      return 45;
    case 'hard':
      return 55;
    default:
      return 45;
  }
};

const createPuzzleFromFullBoard = (difficulty: Difficulty, fullBoard: GameBoard): GameBoard => {
  const puzzle = fullBoard.map((row) => [...row]);
  const positions = Array.from({ length: 81 }, (_, idx) => idx);
  const toRemove = shuffleArray(positions).slice(0, getRemovalCount(difficulty));

  for (const idx of toRemove) {
    const row = Math.floor(idx / 9);
    const col = idx % 9;
    puzzle[row][col] = 0;
  }

  return puzzle;
};

export const generatePuzzle = (difficulty: Difficulty, regionMap: RegionMap): GameBoard =>
  createPuzzleFromFullBoard(difficulty, generateFullBoard(regionMap));

export const generatePuzzleForMode = (difficulty: Difficulty, mode: GameMode): GeneratedPuzzle => {
  if (mode === 'standard') {
    const regionMap = cloneRegionMap(STANDARD_REGION_MAP);
    return {
      puzzle: createPuzzleFromFullBoard(difficulty, generateRandomStandardFullBoard()),
      regionMap,
    };
  }

  const { fullBoard, regionMap } = generateSolvedIrregularSudoku();
  return {
    puzzle: createPuzzleFromFullBoard(difficulty, fullBoard),
    regionMap,
  };
};

export const getRegionNeighbors = (regionMap: RegionMap, row: number, col: number) => {
  const regionId = getRegionId(regionMap, row, col);

  return {
    top: row === 0 || getRegionId(regionMap, row - 1, col) !== regionId,
    right: col === 8 || getRegionId(regionMap, row, col + 1) !== regionId,
    bottom: row === 8 || getRegionId(regionMap, row + 1, col) !== regionId,
    left: col === 0 || getRegionId(regionMap, row, col - 1) !== regionId,
  };
};

export const getRegionMapByMode = (mode: GameMode): RegionMap =>
  mode === 'irregular' ? generateIrregularRegionMap() : cloneRegionMap(STANDARD_REGION_MAP);
