import { eq, sql, and, gt, isNull, isNotNull, count, sum } from "drizzle-orm";
import type { DB } from "../db/connection";
import {
  user_points,
  user_stats,
  recordings,
  recording_plays,
  comments,
  user_achievements,
  achievements,
  users,
} from "../db/schema/index";

export const POINT_VALUES = {
  RECORDING_CREATE: 50,
  RECORDING_PLAY: 10,
  RECORDING_COMPLETE: 5,
  COMMENT_CREATE: 5,
  RECORDING_FEATURED: 100,
} as const;

const DECAY_LAMBDA = 0.005;

export async function awardPoints(
  db: DB,
  userId: string,
  action: keyof typeof POINT_VALUES,
  recordingId?: string,
): Promise<void> {
  const points = POINT_VALUES[action];
  await db
    .insert(user_points)
    .values({
      user_id: userId,
      action,
      points,
      recording_id: recordingId || null,
      date_created: new Date(),
    })
    .onConflictDoNothing();
  await updateUserStats(db, userId);
}

export async function revokePoints(
  db: DB,
  userId: string,
  action: keyof typeof POINT_VALUES,
  recordingId?: string,
): Promise<void> {
  const recordingCondition = recordingId
    ? eq(user_points.recording_id, recordingId)
    : isNull(user_points.recording_id);

  // When no recordingId (e.g. COMMENT_CREATE), delete only one row so each
  // revoke undoes exactly one prior award.
  if (!recordingId) {
    const row = await db
      .select({ id: user_points.id })
      .from(user_points)
      .where(
        and(eq(user_points.user_id, userId), eq(user_points.action, action), recordingCondition),
      )
      .limit(1);
    if (row[0]) {
      await db.delete(user_points).where(eq(user_points.id, row[0].id));
    }
  } else {
    await db
      .delete(user_points)
      .where(
        and(eq(user_points.user_id, userId), eq(user_points.action, action), recordingCondition),
      );
  }

  await updateUserStats(db, userId);
}

export async function calculateWeightedScore(db: DB, userId: string): Promise<number> {
  const result = await db.execute(sql`
    SELECT SUM(
      points * EXP(${sql.raw(String(-DECAY_LAMBDA))} * EXTRACT(EPOCH FROM (NOW() - date_created)) / 86400)
    ) as weighted_score
    FROM user_points
    WHERE user_id = ${userId}
  `);
  return parseFloat((result.rows[0] as { weighted_score?: string })?.weighted_score || "0");
}

export async function updateUserStats(db: DB, userId: string): Promise<void> {
  const now = new Date();

  const rawPointsResult = await db
    .select({ total: sum(user_points.points) })
    .from(user_points)
    .where(eq(user_points.user_id, userId));
  const totalRawPoints = parseInt(String(rawPointsResult[0]?.total || "0"));

  const totalWeightedPoints = await calculateWeightedScore(db, userId);

  const recordingsResult = await db
    .select({ count: count() })
    .from(recordings)
    .where(and(eq(recordings.user_id, userId), eq(recordings.status, "published")));
  const recordingsCount = recordingsResult[0]?.count || 0;

  // Get playbacks count (excluding own recordings)
  const ownRecordingIds = await db
    .select({ id: recordings.id })
    .from(recordings)
    .where(eq(recordings.user_id, userId));
  const ownIds = ownRecordingIds.map((r) => r.id);

  let playbacksCount: number;
  if (ownIds.length > 0) {
    const playbacksResult = await db.execute(sql`
      SELECT COUNT(*) as count FROM recording_plays
      WHERE user_id = ${userId}
      AND recording_id NOT IN (${sql.join(
        ownIds.map((id) => sql`${id}`),
        sql`, `,
      )})
    `);
    playbacksCount = parseInt((playbacksResult.rows[0] as { count?: string })?.count || "0");
  } else {
    const playbacksResult = await db
      .select({ count: count() })
      .from(recording_plays)
      .where(eq(recording_plays.user_id, userId));
    playbacksCount = playbacksResult[0]?.count || 0;
  }

  const commentsResult = await db
    .select({ count: count() })
    .from(comments)
    .where(and(eq(comments.user_id, userId), eq(comments.collection, "videos")));
  const commentsCount = commentsResult[0]?.count || 0;

  const achievementsResult = await db
    .select({ count: count() })
    .from(user_achievements)
    .where(and(eq(user_achievements.user_id, userId), isNotNull(user_achievements.date_unlocked)));
  const achievementsCount = achievementsResult[0]?.count || 0;

  const existing = await db
    .select()
    .from(user_stats)
    .where(eq(user_stats.user_id, userId))
    .limit(1);

  if (existing.length > 0) {
    await db
      .update(user_stats)
      .set({
        total_raw_points: totalRawPoints,
        total_weighted_points: totalWeightedPoints,
        recordings_count: recordingsCount,
        playbacks_count: playbacksCount,
        comments_count: commentsCount,
        achievements_count: achievementsCount,
        last_updated: now,
      })
      .where(eq(user_stats.user_id, userId));
  } else {
    await db.insert(user_stats).values({
      user_id: userId,
      total_raw_points: totalRawPoints,
      total_weighted_points: totalWeightedPoints,
      recordings_count: recordingsCount,
      playbacks_count: playbacksCount,
      comments_count: commentsCount,
      achievements_count: achievementsCount,
      last_updated: now,
    });
  }
}

export async function checkAchievements(db: DB, userId: string, category?: string): Promise<void> {
  let achievementsQuery = db
    .select()
    .from(achievements)
    .where(eq(achievements.status, "published"));

  if (category) {
    achievementsQuery = db
      .select()
      .from(achievements)
      .where(and(eq(achievements.status, "published"), eq(achievements.category, category)));
  }

  const achievementsList = await achievementsQuery;

  for (const achievement of achievementsList) {
    const progress = await getAchievementProgress(db, userId, achievement);

    const existing = await db
      .select()
      .from(user_achievements)
      .where(
        and(
          eq(user_achievements.user_id, userId),
          eq(user_achievements.achievement_id, achievement.id),
        ),
      )
      .limit(1);

    const isUnlocked = progress >= achievement.required_count;
    const wasUnlocked = existing[0]?.date_unlocked !== null;

    if (existing.length > 0) {
      await db
        .update(user_achievements)
        .set({
          progress,
          date_unlocked: isUnlocked
            ? (existing[0].date_unlocked ?? new Date())
            : existing[0].date_unlocked,
        })
        .where(
          and(
            eq(user_achievements.user_id, userId),
            eq(user_achievements.achievement_id, achievement.id),
          ),
        );
    } else {
      await db.insert(user_achievements).values({
        user_id: userId,
        achievement_id: achievement.id,
        progress,
        date_unlocked: isUnlocked ? new Date() : null,
      });
    }

    if (isUnlocked && !wasUnlocked && achievement.points_reward > 0) {
      await db.insert(user_points).values({
        user_id: userId,
        action: `ACHIEVEMENT_${achievement.code}`,
        points: achievement.points_reward,
        recording_id: null,
        date_created: new Date(),
      });
      await updateUserStats(db, userId);
    }
  }
}

async function getAchievementProgress(
  db: DB,
  userId: string,
  achievement: typeof achievements.$inferSelect,
): Promise<number> {
  const { code } = achievement;

  if (["first_recording", "recording_10", "recording_50", "recording_100"].includes(code)) {
    const result = await db
      .select({ count: count() })
      .from(recordings)
      .where(and(eq(recordings.user_id, userId), eq(recordings.status, "published")));
    return result[0]?.count || 0;
  }

  if (code === "featured_recording") {
    const result = await db
      .select({ count: count() })
      .from(recordings)
      .where(
        and(
          eq(recordings.user_id, userId),
          eq(recordings.status, "published"),
          eq(recordings.featured, true),
        ),
      );
    return result[0]?.count || 0;
  }

  if (["first_play", "play_100", "play_500"].includes(code)) {
    const result = await db.execute(sql`
      SELECT COUNT(*) as count
      FROM recording_plays rp
      LEFT JOIN recordings r ON rp.recording_id = r.id
      WHERE rp.user_id = ${userId}
      AND r.user_id != ${userId}
    `);
    return parseInt((result.rows[0] as { count?: string })?.count || "0");
  }

  if (["completionist_10", "completionist_100"].includes(code)) {
    const result = await db
      .select({ count: count() })
      .from(recording_plays)
      .where(and(eq(recording_plays.user_id, userId), eq(recording_plays.completed, true)));
    return result[0]?.count || 0;
  }

  if (["first_comment", "comment_50", "comment_250"].includes(code)) {
    const result = await db
      .select({ count: count() })
      .from(comments)
      .where(and(eq(comments.user_id, userId), eq(comments.collection, "videos")));
    return result[0]?.count || 0;
  }

  if (code === "early_adopter") {
    const user = await db.select().from(users).where(eq(users.id, userId)).limit(1);
    if (user[0]) {
      const joinDate = new Date(user[0].date_created);
      const platformLaunch = new Date("2025-01-01");
      const oneMonthAfterLaunch = new Date(platformLaunch);
      oneMonthAfterLaunch.setMonth(oneMonthAfterLaunch.getMonth() + 1);
      return joinDate <= oneMonthAfterLaunch ? 1 : 0;
    }
  }

  if (code === "one_year") {
    const user = await db.select().from(users).where(eq(users.id, userId)).limit(1);
    if (user[0]) {
      const joinDate = new Date(user[0].date_created);
      const oneYearAgo = new Date();
      oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
      return joinDate <= oneYearAgo ? 1 : 0;
    }
  }

  if (code === "balanced_creator") {
    const recordingsResult = await db
      .select({ count: count() })
      .from(recordings)
      .where(and(eq(recordings.user_id, userId), eq(recordings.status, "published")));
    const playsResult = await db.execute(sql`
      SELECT COUNT(*) as count FROM recording_plays rp
      LEFT JOIN recordings r ON rp.recording_id = r.id
      WHERE rp.user_id = ${userId} AND r.user_id != ${userId}
    `);
    const rc = recordingsResult[0]?.count || 0;
    const pc = parseInt((playsResult.rows[0] as { count?: string })?.count || "0");
    return rc >= 50 && pc >= 100 ? 1 : 0;
  }

  if (code === "top_10_rank") {
    const userStat = await db
      .select()
      .from(user_stats)
      .where(eq(user_stats.user_id, userId))
      .limit(1);
    if (!userStat[0]) return 0;
    const rankResult = await db
      .select({ count: count() })
      .from(user_stats)
      .where(gt(user_stats.total_weighted_points, userStat[0].total_weighted_points || 0));
    const userRank = (rankResult[0]?.count || 0) + 1;
    return userRank <= 10 ? 1 : 0;
  }

  return 0;
}

export async function recalculateAllWeightedScores(db: DB): Promise<void> {
  const allUsers = await db.select({ user_id: user_stats.user_id }).from(user_stats);
  for (const u of allUsers) {
    await updateUserStats(db, u.user_id);
  }
}
