/**
 * performActivity handler
 *
 * Gives the player proactive agency. Instead of only reacting to event prompts,
 * the player can choose to spend the current or next free daily-plan slot on a
 * concrete action (study / exercise / socialize / side-hustle / hobby), paying an
 * energy cost in exchange for the activity's stat / money / affinity tradeoffs.
 *
 * Two modes (both share the same PLAYER_ACTIVITIES effect numbers):
 *  - immediate (default): apply the activity now, consuming a free slot in the
 *    current daily plan and applying the stat/energy/money deltas right away.
 *  - override ({ override: true }): queue the activity for the upcoming
 *    evening/weekend slot so getDailyPlan/getRandomEveningActivity honors the
 *    player's choice instead of the random roll. Persisted via player.c.plannedActivity.
 */

import type { PlayerSession } from '../game/PlayerSession.js';
import { resolvePayloadId } from './payloadHelpers.js';
import {
  PLAYER_ACTIVITIES,
  getPlayerActivity,
  resolveActivityOutcome,
  rollOutcomeTier,
  skillTierLabel,
  activityOutcomeMessage,
  type PlayerActivity,
  type ActivityOutcomeTier,
} from '../game/engine/intradayActivity.js';
import { clampPlayerStats } from '../utils/statUtils.js';
import type { Person } from '../models/Person.js';
import { updateQuestProgress, sendQuestProgress } from '../services/retention/index.js';

// Daily-plan entries are DailyEvent-shaped ({ time, location, title, name }) even
// though Person types dailyPlan as Schedule[]. This local shape lets us read the
// slot fields we mutate without fighting the looser Schedule type.
interface PlanSlot {
  time?: number;
  location?: string;
  title?: string;
  name?: string;
}

// Daily-plan entry names that represent a free / discretionary slot the player
// may overwrite. Fixed obligations (work, school, lunch, sleep, bed) are left
// untouched so the player can't skip school or skip sleep via this command.
const FREE_SLOT_NAMES = new Set<string>([
  'home',
  'relax',
  'leisure',
  'entertainment',
  'gaming',
  'music',
  'reading',
  'hobby',
  'social',
  'exercise',
  'walk',
  'chores',
  'cooking',
  'weekend',
  'rest',
  'play',
  'errands',
  'shopping',
  'study',
  'side-hustle',
]);

function resolveActivityId(payload: unknown): string | undefined {
  return resolvePayloadId(payload, 'activityId', 'activity', 'id');
}

function isOverrideRequest(payload: unknown): boolean {
  if (payload && typeof payload === 'object') {
    const obj = payload as Record<string, unknown>;
    return obj.override === true || obj.mode === 'override' || obj.slot === 'evening';
  }
  return false;
}

/** Outcome of applying one performance of an activity (for the client payload). */
interface ActivityOutcome {
  /** Skill level BEFORE this performance (what scaled the gains). */
  skillLevel: number;
  /** Skill level AFTER this performance (level + 1, +1 more on a breakthrough). */
  nextSkillLevel: number;
  /** Human-readable mastery label for the new skill level (novice..expert). */
  skillTier: string;
  /** Rolled variance tier for this performance. */
  outcomeTier: ActivityOutcomeTier;
  /** Actual deltas applied (skill- and tier-scaled), for client feedback. */
  deltas: PlayerActivity['effects'];
}

/**
 * Apply an activity's effects to the player character with skill progression
 * and a varied per-performance outcome.
 *
 * Energy is consumed from the live `energy` reserve at the FLAT activity cost
 * (cost is not scaled — the energy gate stays predictable). The stat/resource
 * gains are scaled by the current per-activity skill level AND the rolled
 * outcome tier, then applied. The activity's skill is incremented (persisted on
 * person.skills) so practice compounds. Stats are clamped defensively (the daily
 * clampPlayerStats would also correct them, but applying bounds here keeps
 * intra-day state sane for the immediate client update).
 *
 * @param rng injectable RNG (defaults to Math.random) so tests are deterministic.
 */
function applyActivityEffects(
  person: Person,
  activity: PlayerActivity,
  rng: () => number = Math.random
): ActivityOutcome {
  person.energy = Math.max(0, (person.energy ?? 0) - activity.energyCost);

  if (!person.skills) person.skills = {};
  const skillLevel = person.skills[activity.id] ?? 0;

  const tier = rollOutcomeTier(rng);
  const { deltas, nextSkillLevel } = resolveActivityOutcome(activity, skillLevel, tier);

  if (deltas.intelligence) person.intelligence = (person.intelligence ?? 0) + deltas.intelligence;
  if (deltas.social) person.social = (person.social ?? 0) + deltas.social;
  if (deltas.creativity) person.creativity = (person.creativity ?? 0) + deltas.creativity;
  if (deltas.happiness) person.happiness = (person.happiness ?? 0) + deltas.happiness;
  if (deltas.health) person.health = (person.health ?? 0) + deltas.health;
  if (deltas.stress) person.stress = (person.stress ?? 0) + deltas.stress;
  if (deltas.money) person.money = (person.money ?? 0) + deltas.money;
  if (deltas.affinity) person.affinity = (person.affinity ?? 0) + deltas.affinity;

  person.skills[activity.id] = nextSkillLevel;

  return {
    skillLevel,
    nextSkillLevel,
    skillTier: skillTierLabel(nextSkillLevel),
    outcomeTier: tier,
    deltas,
  };
}

/**
 * Find the current or next free daily-plan slot at or after the current hour
 * that the player may overwrite with a chosen activity.
 */
function findFreeSlot(person: Person, currentHour: number): PlanSlot | undefined {
  const plan = (person.dailyPlan ?? []) as unknown as PlanSlot[];
  let best: PlanSlot | undefined;
  for (const entry of plan) {
    const time = entry.time;
    const name = entry.name ?? '';
    if (typeof time !== 'number') continue;
    if (time < currentHour) continue;
    if (!FREE_SLOT_NAMES.has(name)) continue;
    if (best === undefined || (best.time ?? Infinity) > time) {
      best = entry;
    }
  }
  return best;
}

/**
 * Fire the daily/weekly quest progress hooks for a successfully-performed
 * immediate activity. Each quest type is fired AT MOST ONCE per action:
 *   - complete_activities: every successful immediate activity (+1).
 *   - spend_energy:        the activity's flat energyCost actually spent.
 *   - study:               only when the performed activity is the study activity.
 *   - increase_affinity:   only on the socialize path (the activity that, by
 *                          design, represents reaching out to a relationship).
 * Matches the conversations.ts / purchases.ts / romance.ts hook pattern
 * (updateQuestProgress + sendQuestProgress).
 */
async function fireActivityQuestProgress(
  session: PlayerSession,
  activity: PlayerActivity
): Promise<void> {
  const player = session.player;

  const complete = await updateQuestProgress(player.userId, 'complete_activities', 1, player);
  if (complete) sendQuestProgress(session, complete);

  if (activity.energyCost > 0) {
    const energy = await updateQuestProgress(
      player.userId,
      'spend_energy',
      activity.energyCost,
      player
    );
    if (energy) sendQuestProgress(session, energy);
  }

  if (activity.id === 'study') {
    const study = await updateQuestProgress(player.userId, 'study', 1, player);
    if (study) sendQuestProgress(session, study);
  }

  if (activity.id === 'socialize') {
    const affinity = await updateQuestProgress(player.userId, 'increase_affinity', 1, player);
    if (affinity) sendQuestProgress(session, affinity);

    const social = await updateQuestProgress(player.userId, 'socialize', 1, player);
    if (social) sendQuestProgress(session, social);
  }
}

export async function handlePerformActivity(
  payload: unknown,
  session: PlayerSession,
  rng: () => number = Math.random
): Promise<void> {
  const player = session.player;
  const person = player.c;
  const activityId = resolveActivityId(payload);

  if (!activityId) {
    session.send({
      type: 'error',
      message: 'No activity specified',
    });
    return;
  }

  const activity = getPlayerActivity(activityId);
  if (!activity) {
    session.send({
      type: 'error',
      message: `Unknown activity: ${activityId}`,
      availableActivities: Object.keys(PLAYER_ACTIVITIES),
    });
    return;
  }

  // Age-appropriateness check (e.g. side-hustle has a minimum age).
  if (activity.minAge !== undefined && (person.ageYears ?? 0) < activity.minAge) {
    session.send({
      type: 'error',
      message: `You are too young to ${activity.title.replace(/^You /, '').toLowerCase()}`,
    });
    return;
  }

  // Energy gate: cannot perform an activity without enough energy to pay its cost.
  if ((person.energy ?? 0) < activity.energyCost) {
    session.send({
      type: 'error',
      message: 'Not enough energy for this activity',
      required: activity.energyCost,
      current: person.energy ?? 0,
    });
    return;
  }

  // Override mode: queue the choice for the upcoming evening/weekend slot.
  if (isOverrideRequest(payload)) {
    person.plannedActivity = activity.id;
    await session.savePlayer();
    session.sendPlayerObject();
    session.send({
      type: 'activityPlanned',
      success: true,
      activityId: activity.id,
      name: activity.name,
      title: activity.title,
      message: `Planned: ${activity.title}`,
    });
    return;
  }

  // Immediate mode: must have a free slot to spend right now.
  const currentHour = player.hourOfDay ?? 0;
  const slot = findFreeSlot(person, currentHour);
  if (!slot) {
    session.send({
      type: 'error',
      message: 'No free time slot available for an activity right now',
    });
    return;
  }

  // Spend the slot: rewrite it to the chosen activity and apply effects.
  slot.title = activity.title;
  slot.name = activity.name;
  const outcome = applyActivityEffects(person, activity, rng);
  clampPlayerStats(player);

  // Daily-quest progress (fires EXACTLY ONCE per successful immediate activity).
  // This is the single call site for these quest types — none of them are also
  // advanced by an offline/online loop tick (the offline GameEngine path does not
  // run handlePerformActivity, and these types have no loop-side hook), so there
  // is no double-count risk. `updateQuestProgress` is safe to call here because
  // handlePerformActivity always has a live session.
  await fireActivityQuestProgress(session, activity);

  await session.savePlayer();
  session.sendPlayerObject();
  session.send({
    type: 'activityPerformed',
    success: true,
    activityId: activity.id,
    name: activity.name,
    title: activity.title,
    energyCost: activity.energyCost,
    // Base activity effects (unscaled, for reference/back-compat).
    effects: activity.effects,
    // Skill progression + varied-outcome feedback (T006): the actual scaled
    // deltas applied, which outcome tier was rolled, and the resulting skill so
    // the client can show real feedback ("Great study session! Study skill 6").
    outcomeTier: outcome.outcomeTier,
    skillLevel: outcome.nextSkillLevel,
    skillTier: outcome.skillTier,
    deltas: outcome.deltas,
    message: activityOutcomeMessage(activity, outcome.outcomeTier),
  });
}
