import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model, Types } from "mongoose";
import {
  Achievement,
  AchievementDocument,
} from "./schemas/achievement.schema";
import {
  Challenges,
  ChallengesDocument,
} from "./schemas/challenges.schema";
import {
  ChallengesUser,
  ChallengesUserDocument,
} from "./schemas/challengesUser.schema";
import {
  ChallengeMode,
  ChallengeProgressStatus,
} from "src/common/enums/challenges.enum";
import {
  calculateHabitProgress,
  resolveHabitCadence,
} from "src/common/utils/habitWindow.helper";
import {
  performanceProgress,
  resolveAttemptStatus,
  volumeProgress,
} from "src/common/utils/challengeProgress.helper";

/**
 * Owns the `achievements` collection — a derived projection of completed
 * challenges. `challengesusers` stays the source of truth for attempts/progress;
 * this service upserts a completion here (keyed by user+challenge) whenever one
 * is detected, and serves the achievements/completed reads. Self-contained (uses
 * the pure progress helpers) so both ChallengesService and ProgressService can
 * call it without cross-service coupling.
 */
@Injectable()
export class AchievementsService {
  constructor(
    @InjectModel(Achievement.name)
    private achievementModel: Model<AchievementDocument>,
    @InjectModel(Challenges.name)
    private challengesModel: Model<ChallengesDocument>,
    @InjectModel(ChallengesUser.name)
    private membershipModel: Model<ChallengesUserDocument>
  ) {}

  /** The final progress summary + live status for one membership. */
  private evaluate(challenge: any, membership: any, now: Date) {
    const mode = challenge?.type ?? membership?.mode;
    if (mode === ChallengeMode.HABIT) {
      const state = calculateHabitProgress(
        resolveHabitCadence(challenge),
        membership.started_at ?? membership.createdAt,
        membership.progress,
        now
      );
      return {
        mode,
        status: state.status,
        result: { target: state.target, completed: state.completed_count },
      };
    }
    const summary =
      mode === ChallengeMode.VOLUME
        ? volumeProgress(challenge, membership)
        : performanceProgress(challenge, membership);
    return {
      mode,
      status: resolveAttemptStatus(challenge, membership, summary, now),
      result: summary,
    };
  }

  /** Build the upsert payload snapshotting the challenge as-completed. */
  private snapshot(
    challenge: any,
    membership: any,
    mode: ChallengeMode,
    result: any,
    completedAt: Date
  ) {
    return {
      membership_id: membership._id,
      type: mode ?? null,
      title: challenge.title ?? null,
      short_desc: challenge.short_desc ?? null,
      focus: challenge.focus ?? null,
      level: challenge.level ?? null,
      attachment: challenge.attachment ?? null,
      goal: challenge.goal ?? null,
      duration: challenge.duration ?? 0,
      result: result ?? null,
      started_at: membership.started_at ?? membership.createdAt ?? null,
      completed_at: completedAt,
    };
  }

  /**
   * Upsert one completion (idempotent, keyed by user+challenge). Safe to call
   * from any completion-detection point; concurrent callers write the same doc.
   */
  async record(params: {
    userId: any;
    challenge: any;
    membership: any;
    mode: ChallengeMode;
    result: any;
    completedAt?: Date;
  }) {
    const { userId, challenge, membership, mode, result } = params;
    const completedAt =
      params.completedAt ?? membership.finished_at ?? new Date();
    await this.achievementModel.updateOne(
      { user_id: userId, challenge_id: challenge._id },
      {
        $set: this.snapshot(challenge, membership, mode, result, completedAt),
        $setOnInsert: {
          user_id: userId,
          challenge_id: challenge._id,
        },
      },
      { upsert: true }
    );
  }

  /**
   * Recompute the user's live memberships, persist any status transition back to
   * `challengesusers`, and upsert every completion into `achievements`. Call this
   * before reading achievements / the completed tab so time-based completions
   * (which happen with no user action) are captured. Also lazily backfills
   * already-completed records that predate this collection.
   */
  async reconcileUser(userId: any) {
    const memberships = await this.membershipModel
      .find({
        user_id: userId,
        $or: [
          { deleted_at: null },
          { status: ChallengeProgressStatus.COMPLETED },
        ],
      })
      .lean();
    if (!memberships.length) return;

    const challenges = await this.challengesModel
      .find({ _id: { $in: memberships.map((m) => m.challenge_id) } })
      .lean();
    const byId = new Map(challenges.map((c) => [String(c._id), c]));

    const now = new Date();
    const statusOps: any[] = [];
    // one completion per challenge — the LATEST wins. A challenge completed
    // twice (old retired attempt + new one) has two completed records; without
    // this the upsert order is undefined and could regress the achievement to
    // the older completion.
    const latestByChallenge = new Map<string, any>();

    for (const m of memberships) {
      const challenge: any = byId.get(String(m.challenge_id));
      if (!challenge) continue;

      const { mode, status, result } = this.evaluate(challenge, m, now);

      // persist a status transition on the live record
      const isLive = m.deleted_at == null;
      if (isLive && status !== m.status) {
        statusOps.push({
          updateOne: {
            filter: { _id: m._id },
            update: {
              $set: {
                status,
                finished_at:
                  status === ChallengeProgressStatus.ACTIVE ? null : now,
              },
            },
          },
        });
      }

      if (status === ChallengeProgressStatus.COMPLETED) {
        const completedAt = m.finished_at ?? now;
        const key = String(m.challenge_id);
        const prev = latestByChallenge.get(key);
        if (
          !prev ||
          new Date(completedAt).getTime() >=
            new Date(prev.completedAt).getTime()
        ) {
          latestByChallenge.set(key, {
            challenge,
            membership: m,
            mode,
            result,
            completedAt,
          });
        }
      }
    }

    if (statusOps.length) await this.membershipModel.bulkWrite(statusOps);

    // upsert only the most-recent completion per challenge
    for (const c of latestByChallenge.values()) {
      await this.record({
        userId,
        challenge: c.challenge,
        membership: c.membership,
        mode: c.mode,
        result: c.result,
        completedAt: c.completedAt,
      });
    }
  }

  /** Paginated achievements for a user, newest first, localized. */
  async listForUser(userId: any, lang = "en", page = 1, limit = 10) {
    page = Number(page) || 1;
    limit = Number(limit) || 10;
    const skip = (page - 1) * limit;

    const match = { user_id: new Types.ObjectId(String(userId)) };
    const [rows, total] = await Promise.all([
      this.achievementModel
        .find(match)
        .sort({ completed_at: -1 })
        .skip(skip)
        .limit(limit)
        .lean(),
      this.achievementModel.countDocuments(match),
    ]);

    const items = rows.map((a: any) => ({
      _id: a._id,
      challenge_id: a.challenge_id,
      membership_id: a.membership_id,
      type: a.type ?? null,
      title: a.title?.[lang] ?? null,
      short_desc: a.short_desc?.[lang] ?? null,
      focus: a.focus?.[lang] ?? null,
      level: a.level?.[lang] ?? null,
      attachment: a.attachment ?? null,
      goal: a.goal ?? null,
      duration: a.duration ?? 0,
      result: a.result ?? null,
      started_at: a.started_at ?? null,
      completed_at: a.completed_at ?? null,
    }));

    return {
      items,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  /** Has this user ever completed this challenge? */
  async hasCompleted(userId: any, challengeId: any): Promise<boolean> {
    const count = await this.achievementModel.countDocuments({
      user_id: new Types.ObjectId(String(userId)),
      challenge_id: new Types.ObjectId(String(challengeId)),
    });
    return count > 0;
  }

  /** Set of challenge ids (as strings) this user has completed at least once. */
  async completedChallengeIds(userId: any): Promise<Set<string>> {
    const ids = await this.achievementModel.distinct("challenge_id", {
      user_id: new Types.ObjectId(String(userId)),
    });
    return new Set(ids.map((id: any) => String(id)));
  }
}
