import { Types } from "mongoose";
import ZiplinePuzzle from "../models/ziplinePuzzle";
import ZiplineAttempt from "../models/ziplineAttempt";
import User from "../models/users";
import { toKolkataTime } from "../utils/timezone";
import { validateSubmittedPath } from "../utils/ZiplineValidation";
import { formatDuration } from "../utils/formatDuration";

type Coord = [number, number];

const ZIPLINE_PUZZLE_PUBLIC_PROJECTION =
  "_id title description difficulty grid numbers blocks is_active";

function isValidCoordTuple(value: any): value is Coord {
  return (
    Array.isArray(value) &&
    value.length === 2 &&
    Number.isInteger(value[0]) &&
    Number.isInteger(value[1])
  );
}

function coordKey(row: number, col: number): string {
  return `${row},${col}`;
}

function isInBounds(
  row: number,
  col: number,
  rows: number,
  cols: number,
): boolean {
  return row >= 0 && row < rows && col >= 0 && col < cols;
}

function validateAndNormalizePuzzlePayload({
  title,
  description,
  difficulty,
  grid,
  numbers,
  blocks,
}: any) {
  if (!title || typeof title !== "string") {
    throw new Error("title is required");
  }

  if (!description || typeof description !== "string") {
    throw new Error("description is required");
  }

  if (!["easy", "medium", "hard"].includes(difficulty)) {
    throw new Error("difficulty must be one of: easy, medium, hard");
  }

  if (!Array.isArray(grid) || grid.length === 0 || !grid.every(Array.isArray)) {
    throw new Error("grid must be a non-empty 2D array");
  }

  const rows = grid.length;
  const cols = grid[0]?.length;

  if (!cols || !grid.every((row: any[]) => row.length === cols)) {
    throw new Error("grid must be rectangular with at least 1 column");
  }

  if (!Array.isArray(numbers) || numbers.length === 0) {
    throw new Error("numbers must be a non-empty array");
  }

  if (!Array.isArray(blocks)) {
    throw new Error("blocks must be an array");
  }

  const checkpointValues = new Set<number>();
  const checkpointCoordsByValue = new Map<number, Coord>();
  const checkpointCoordKeys = new Set<string>();

  for (const item of numbers) {
    const { row, col, value } = item || {};

    if (![row, col, value].every(Number.isInteger)) {
      throw new Error(
        "each numbers item must have integer row, col, and value",
      );
    }

    if (!isInBounds(row, col, rows, cols)) {
      throw new Error("numbers coordinates must be within grid bounds");
    }

    if (value < 1) {
      throw new Error(
        "numbers values must start at 1 and increase sequentially",
      );
    }

    if (checkpointValues.has(value)) {
      throw new Error("numbers values must be unique");
    }

    const key = coordKey(row, col);
    if (checkpointCoordKeys.has(key)) {
      throw new Error("numbers coordinates must be unique");
    }

    checkpointValues.add(value);
    checkpointCoordsByValue.set(value, [row, col]);
    checkpointCoordKeys.add(key);
  }

  const sortedValues = [...checkpointValues].sort((a, b) => a - b);
  for (let i = 0; i < sortedValues.length; i += 1) {
    if (sortedValues[i] !== i + 1) {
      throw new Error("numbers values must be sequential: 1..N");
    }
  }

  for (const row of grid) {
    for (const cell of row) {
      if (!Number.isInteger(cell)) {
        throw new Error("grid must contain only integer values");
      }
    }
  }

  const blockedCoordKeys = new Set<string>();
  for (const item of blocks) {
    const { row, col } = item || {};
    if (![row, col].every(Number.isInteger)) {
      throw new Error("each blocks item must have integer row and col");
    }
    if (!isInBounds(row, col, rows, cols)) {
      throw new Error("blocks coordinates must be within grid bounds");
    }
    blockedCoordKeys.add(coordKey(row, col));
  }

  return {
    title: title.trim(),
    description: description.trim(),
    difficulty,
    grid,
    numbers,
    blocks,
  };
}

// ─────────────────────────────────────────────────────────────
// Fetch all active puzzles/games (without solutions)
// ─────────────────────────────────────────────────────────────
export async function getAllPuzzles() {
  return ZiplinePuzzle.find()
    .select(ZIPLINE_PUZZLE_PUBLIC_PROJECTION)
    .sort({
      createdAt: -1,
    });
}

// ─────────────────────────────────────────────────────────────
// Fetch random puzzle by difficulty
// ─────────────────────────────────────────────────────────────
export async function getRandomPuzzle(difficulty?: "easy" | "medium" | "hard") {
  const query = difficulty
    ? { difficulty, is_active: true }
    : { is_active: true };

  const count = await ZiplinePuzzle.countDocuments(query);

  if (count === 0) {
    throw new Error("No puzzles available");
  }

  const random = Math.floor(Math.random() * count);

  const puzzle = await ZiplinePuzzle.findOne(query)
    .select(ZIPLINE_PUZZLE_PUBLIC_PROJECTION)
    .skip(random);

  return puzzle;
}

// ─────────────────────────────────────────────────────────────
// Start new attempt
// ─────────────────────────────────────────────────────────────
export async function startAttempt({
  userId,
  puzzleId,
}: {
  userId: Types.ObjectId;
  puzzleId: Types.ObjectId;
}) {
  const puzzle = await ZiplinePuzzle.findById(puzzleId).select("_id");

  if (!puzzle) {
    throw new Error("Puzzle not found");
  }

  const existingAttempt = await ZiplineAttempt.findOne({
    user: userId,
    puzzle: puzzleId,
    status: { $in: ["IN_PROGRESS", "COMPLETED"] },
  });

  if (existingAttempt?.status === "COMPLETED") {
    const error = new Error("You have already completed this puzzle");
    (error as any).statusCode = 403;
    throw error;
  }

  if (existingAttempt?.status === "IN_PROGRESS") {
    return existingAttempt;
  }

  const attempt = await ZiplineAttempt.create({
    user: userId,
    puzzle: puzzleId,
    status: "IN_PROGRESS",
    startedAt: new Date(),
    progress: {},
    score: 0,
    isCorrect: false,
    timeTaken: 0,
  });

  return attempt;
}

// ─────────────────────────────────────────────────────────────
// Update attempt progress
// ─────────────────────────────────────────────────────────────
export async function updateAttemptProgress({
  attemptId,
  progress,
}: {
  attemptId: Types.ObjectId;
  progress: any;
}) {
  const attempt = await ZiplineAttempt.findById(attemptId);

  if (!attempt) {
    throw new Error("Attempt not found");
  }

  if (attempt.status === "COMPLETED") {
    throw new Error("Attempt already completed");
  }

  attempt.progress = progress;

  await attempt.save();

  return attempt;
}

// ─────────────────────────────────────────────────────────────
// Submit attempt
// ─────────────────────────────────────────────────────────────
export async function submitAttempt({
  attemptId,
  submittedSolution,
}: {
  attemptId: Types.ObjectId;
  submittedSolution: any;
}) {
  if (
    !submittedSolution ||
    !Array.isArray(submittedSolution.path) ||
    submittedSolution.path.length < 2
  ) {
    throw new Error("path is required and must be a full path array");
  }

  const submittedPath: Coord[] = submittedSolution.path.map((coord: any) => {
    if (!isValidCoordTuple(coord)) {
      throw new Error(
        "path must be an array of [row, col] integer coordinates",
      );
    }
    return [coord[0], coord[1]];
  });

  const attempt = await ZiplineAttempt.findById(attemptId).populate("puzzle");

  if (!attempt) {
    throw new Error("Attempt not found");
  }

  if (attempt.status === "COMPLETED") {
    throw new Error("Attempt already submitted");
  }

  const puzzle: any = attempt.puzzle;

  if (!puzzle) {
    throw new Error("Puzzle not found");
  }

  if (!attempt.startedAt) {
    throw new Error("Attempt start time missing");
  }

  // Calculate time taken
  const timeTaken = Math.floor(
    (Date.now() - new Date(attempt.startedAt).getTime()) / 1000,
  );

  // ─────────────────────────────────────────────────────────
  // Completion Validation
  // Customize this based on actual game logic
  // ─────────────────────────────────────────────────────────
  // const isCorrect =
  //   JSON.stringify(submittedPath) === JSON.stringify(puzzle.solution?.path || []);
  const isCorrect = validateSubmittedPath(submittedPath, puzzle);

  // Time-based score only (no difficulty): 10..100
  const minScore = 10;
  const maxScore = 100;
  const maxTimeForMinScore = 300; // 5 minutes

  let score = 0;

  if (isCorrect) {
    // Ratio capped between 0 and 1
    const ratio = Math.min(timeTaken / maxTimeForMinScore, 1);

    // Faster time = higher score
    score = Math.round(maxScore - (maxScore - minScore) * ratio);

    // Ensure min/max limits
    score = Math.max(minScore, Math.min(maxScore, score));
  }

  // ─────────────────────────────────────────────────────────
  // Update Attempt
  // ─────────────────────────────────────────────────────────
  attempt.submittedSolution = { path: submittedPath };
  attempt.isCorrect = isCorrect;
  attempt.timeTaken = timeTaken;
  attempt.score = score;
  attempt.completedAt = new Date();
  attempt.status = isCorrect ? "COMPLETED" : "FAILED";

  if (isCorrect && score > 0) {
    await User.findByIdAndUpdate(attempt.user, {
      $inc: {
        earned_points: score,
        zipline_points: score,
      },
    });
  }

  await attempt.save();

  return {
    success: isCorrect,
    isCorrect,
    status: attempt.status,
    score,
    timeTaken: formatDuration(timeTaken),
    startedAt: toKolkataTime(attempt.startedAt),
    completedAt: attempt.completedAt
      ? toKolkataTime(attempt.completedAt)
      : null,
  };
}

// ─────────────────────────────────────────────────────────────
// Get all attempts for user
// ─────────────────────────────────────────────────────────────
export async function getUserAttempts(userId: Types.ObjectId) {
  const attempts = await ZiplineAttempt.find({ user: userId })
    .select("_id puzzle isCorrect score timeTaken startedAt completedAt")
    .populate({
      path: "puzzle",
      select: "_id title difficulty",
    })
    .sort({ createdAt: -1 })
    .lean();

  return attempts.map((attempt: any) => ({
    _id: attempt._id?.toString(),
    puzzleId: attempt.puzzle?._id?.toString() || null,
    title: attempt.puzzle?.title || null,
    difficulty: attempt.puzzle?.difficulty || null,
    isCorrect: Boolean(attempt.isCorrect),
    score: typeof attempt.score === "number" ? attempt.score : 0,
    timeTaken: typeof attempt.timeTaken === "number" ? attempt.timeTaken : 0,
    startedAt: attempt.startedAt ? toKolkataTime(attempt.startedAt) : null,
    completedAt: attempt.completedAt
      ? toKolkataTime(attempt.completedAt)
      : null,
  }));
}

// ─────────────────────────────────────────────────────────────
// Admin - Add new puzzle/game
// ─────────────────────────────────────────────────────────────
export async function addPuzzle({
  grid,
  numbers,
  blocks,
  difficulty,
  title,
  description,
}: any) {
  try {
    const normalizedPuzzle = validateAndNormalizePuzzlePayload({
      grid,
      numbers,
      blocks,
      difficulty,
      title,
      description,
    });

    return ZiplinePuzzle.create({
      ...normalizedPuzzle,
      is_active: true,
    });
  } catch (error: any) {
    if (!error?.statusCode) {
      error.statusCode = 400;
    }
    throw error;
  }
}

export async function updatePuzzle(puzzleId: string, payload: any) {
  try {
    const puzzle = await ZiplinePuzzle.findById(puzzleId);

    if (!puzzle) {
      const error = new Error("Puzzle not found") as Error & {
        statusCode?: number;
      };
      error.statusCode = 404;
      throw error;
    }

    const {
      grid,
      numbers,
      blocks,
      difficulty,
      title,
      description,
      is_active,
    } = payload || {};

    const shouldValidatePuzzleData =
      grid !== undefined ||
      numbers !== undefined ||
      blocks !== undefined ||
      difficulty !== undefined ||
      title !== undefined ||
      description !== undefined;

    if (shouldValidatePuzzleData) {
      const normalizedPuzzle = validateAndNormalizePuzzlePayload({
        grid: grid ?? puzzle.grid,
        numbers: numbers ?? puzzle.numbers,
        blocks: blocks ?? puzzle.blocks,
        difficulty: difficulty ?? puzzle.difficulty,
        title: title ?? puzzle.title,
        description: description ?? puzzle.description,
      });

      puzzle.title = normalizedPuzzle.title;
      puzzle.description = normalizedPuzzle.description;
      puzzle.difficulty = normalizedPuzzle.difficulty;
      puzzle.grid = normalizedPuzzle.grid;
      puzzle.numbers = normalizedPuzzle.numbers;
      puzzle.blocks = normalizedPuzzle.blocks;
    }

    if (typeof is_active === "boolean") {
      puzzle.is_active = is_active;
    }

    await puzzle.save();
    return puzzle;
  } catch (error: any) {
    if (!error?.statusCode) {
      error.statusCode = 400;
    }
    throw error;
  }
}

export async function deletePuzzle(puzzleId: string) {
  const deletedPuzzle = await ZiplinePuzzle.findByIdAndDelete(puzzleId);

  if (!deletedPuzzle) {
    const error = new Error("Puzzle not found") as Error & {
      statusCode?: number;
    };
    error.statusCode = 404;
    throw error;
  }

  return { puzzleId };
}
