import type { DynamicTextFn, EventDefinition, EventPlayerContext } from '../types.js';
import { playerHasFlag } from '../engine/selector.js';
import { pickVariant } from '../../../utils/textVariations.js';

/**
 * Career arc catalog (v2 / live runtime).
 *
 * Ported from the legacy class-based arc `server/src/events/career/careerMilestones.ts`
 * (UNREACHABLE in the v2 runtime) into declarative `EventDefinition`s. This is
 * the first arc ported under the "FLATTEN, don't rebuild the engine" recipe:
 *
 *  - Legacy COMPUTED/RANDOM outcomes (e.g. "raise = salary * 0.25", "60% chance
 *    of success") are approximated with STATIC, sensible fixed deltas. A
 *    promotion gives a fixed money + happiness bump rather than a percentage of
 *    a salary field the v2 context doesn't track. The dominant/representative
 *    branch of each random legacy outcome is kept; the gambling nuance is lost
 *    intentionally (documented per event).
 *  - INDEPENDENT milestones are one `EventDefinition` each, gated by
 *    minAge/maxAge + an `isEligible` occupation check.
 *  - RECURRING events (performance review) are `repeatable` with a ~360-day
 *    cooldown so they re-fire roughly yearly.
 *  - MULTI-STAGE arcs (layoff -> job search, start-business -> outcome) are
 *    modeled as SEPARATE `EventDefinition`s. Stage 2 is gated first on existing
 *    state (`askedQuestions` containing the stage-1 id) plus a minAge proxy,
 *    and where it must branch on WHICH choice was made it reads a persistent
 *    flag set by the stage-1 choice via `setFlags` -> `playerHasFlag`.
 *
 * Occupation model: the v2 context exposes `player.c.occupation` (legacy used
 * 'work' for employed, 'unemployed' otherwise). We gate employed events on
 * `occupation === 'work'`.
 */

function hasOccupation(player: EventPlayerContext, occupation: string): boolean {
  return (player.c.occupation as string | undefined) === occupation;
}

/**
 * Intelligence threshold above which the "high-stakes flagship project" career
 * payoff unlocks (T007). Reading a stat in isEligible is the lever that turns the
 * intelligence trajectory (built by Wave 1's performActivity) into real unlocked
 * content: a sharp mind gets handed the career-defining project that a less
 * studied character is never offered. This gates BONUS content only — it never
 * blocks a milestone or ordinary career progression.
 */
export const FLAGSHIP_PROJECT_INTELLIGENCE = 60;

/** Read a numeric character stat off the event context, defaulting to 50. */
function stat(player: EventPlayerContext, key: string): number {
  const value = (player.c as Record<string, unknown>)[key];
  return typeof value === 'number' ? value : 50;
}

function wasAsked(player: EventPlayerContext, eventId: string): boolean {
  const asked = (player as { askedQuestions?: unknown }).askedQuestions;
  if (asked instanceof Set) return asked.has(eventId);
  if (Array.isArray(asked)) return asked.includes(eventId);
  return false;
}

function choice(
  choiceId: string,
  text: string,
  resolutionText: string,
  overrides?: Partial<EventDefinition['choices'][number]>
): EventDefinition['choices'][number] {
  return {
    choiceId,
    text,
    resolutionText,
    ...overrides,
  };
}

// Flags set by stage-1 choices to drive choice-branching follow-ups.
export const CAREER_FLAGS = {
  startedBusiness: 'career.startedBusiness',
  businessSideHustle: 'career.businessSideHustle',
  wasLaidOff: 'career.wasLaidOff',
} as const;

export const careerCatalog: EventDefinition[] = [
  // ===========================================================================
  // 1. Performance Review (RECURRING ~yearly)
  // Legacy: quarterly, score computed from intelligence/stress/prep + random.
  // Flatten: repeatable yearly review; the player's PREP choice deterministically
  // selects the outcome (prepared = exceeds, feedback = meets, wing = needs work).
  // ===========================================================================
  {
    id: 'performanceReview',
    category: 'career',
    prompt:
      'It is time for your annual performance review. Your manager has scheduled a one-on-one. How do you prepare?',
    minAge: 18,
    maxAge: 70,
    repeatable: true,
    cooldownDays: 360,
    weight: 2,
    isEligible: (player) => hasOccupation(player, 'work'),
    choices: [
      choice(
        'prepared',
        'Prepare a list of accomplishments',
        'You walked in prepared and your manager said you "exceed expectations." You earned a raise and recognition.',
        { energyCost: 5, effects: { stats: { happiness: 10, stress: -5 }, resources: { money: 400 } } }
      ),
      choice(
        'feedback',
        'Ask coworkers for feedback first',
        'Solid review. "Meets expectations" across the board, with a couple of areas to grow in.',
        { energyCost: 3, effects: { stats: { happiness: 3 } } }
      ),
      choice(
        'wing',
        'Wing it - your work speaks for itself',
        'The review did not go well. "Needs improvement" in several areas, and a 30-day improvement plan. A wake-up call.',
        { effects: { stats: { happiness: -8, stress: 10 } } }
      ),
    ],
  },

  // ===========================================================================
  // 2. Promotion Offer (INDEPENDENT, once)
  // Legacy: negotiate had a 60% success roll; "think it over" + "decline"
  // scheduled passive follow-ups. Flatten: negotiate yields the better-terms
  // outcome (representative success path), decline/think collapse into a single
  // declarative resolution (no scheduled message needed - the text carries it).
  // ===========================================================================
  {
    id: 'promotionOffer',
    category: 'career',
    prompt:
      'Your manager calls you in with good news: a senior position is opening up and they want you for it. It comes with a 25% raise and significantly more responsibility.',
    minAge: 22,
    maxAge: 60,
    weight: 1,
    isEligible: (player) => hasOccupation(player, 'work'),
    choices: [
      choice(
        'accept',
        'Accept immediately',
        'Congratulations on the promotion! The workload is heavier but you feel validated.',
        { effects: { stats: { happiness: 5, stress: 10 }, resources: { money: 600 } } }
      ),
      choice(
        'negotiate',
        'Negotiate for a bigger raise',
        'Your negotiation paid off - they bumped the raise higher. Your manager respected that you know your worth.',
        { energyCost: 5, effects: { stats: { happiness: 8, stress: 8 }, resources: { money: 850 } } }
      ),
      choice(
        'decline',
        'Decline - I like my current role',
        'You politely declined. Your manager respected the choice, but warned the offer will not stay open forever.',
        { effects: { stats: { stress: -5 } } }
      ),
    ],
  },

  // ===========================================================================
  // 3. Got Passed Over (INDEPENDENT, once)
  // Legacy: confront/grind/search had random outcomes + scheduled follow-ups.
  // Flatten: representative outcomes, no scheduled message.
  // ===========================================================================
  {
    id: 'gotPassedOver',
    category: 'career',
    prompt:
      'A coworker who started after you just got promoted to the position you have been eyeing. You feel a sting of resentment. How do you handle it?',
    minAge: 25,
    maxAge: 60,
    weight: 1,
    isEligible: (player) => hasOccupation(player, 'work'),
    choices: [
      choice(
        'confront',
        'Talk to your manager about it',
        'Your manager appreciated the honest conversation and said you are next in line. It felt good to advocate for yourself.',
        { energyCost: 5, effects: { stats: { happiness: 3 } } }
      ),
      choice(
        'grind',
        'Channel it into working harder',
        'You poured your frustration into your work. It is exhausting, but your manager has started calling you a top performer.',
        { energyCost: 10, effects: { stats: { stress: 8 } } }
      ),
      choice(
        'search',
        'Start quietly job hunting',
        'You updated your resume and started browsing listings. Scary, but exciting - maybe it is time for a fresh start.',
        { effects: { stats: { happiness: -3 } } }
      ),
      choice(
        'accept',
        'Accept it and move on',
        'You swallowed your pride and congratulated your coworker. It stung, but you will get your chance.',
        { effects: { stats: { happiness: -5 } } }
      ),
    ],
  },

  // ===========================================================================
  // 4. Layoff Notice (MULTI-STAGE, stage 1)
  // Legacy: everyone loses the job; severance computed from salary; internal
  // transfer had a 30% save roll; all branches scheduled a 2-week job-search
  // follow-up. Flatten: stage-1 applies the job loss + a fixed severance, and
  // sets the `wasLaidOff` flag. Stage 2 (job search) is a SEPARATE passive event
  // gated on that flag. The internal-transfer "save" branch is kept as a choice
  // that does NOT set the laid-off flag (so no job-search follow-up fires).
  // ===========================================================================
  {
    id: 'layoffNotice',
    category: 'career',
    prompt:
      'HR calls you into a meeting. "Due to restructuring, your position is being eliminated." Your stomach drops. How do you respond?',
    minAge: 22,
    maxAge: 65,
    weight: 1,
    isEligible: (player) => hasOccupation(player, 'work'),
    choices: [
      choice(
        'negotiate',
        'Negotiate a severance package',
        'You negotiated a generous severance. It is little comfort, but it buys you time to find something new.',
        {
          energyCost: 5,
          effects: {
            stats: { happiness: -15, stress: 20 },
            resources: { money: 1500 },
          },
          setFlags: [CAREER_FLAGS.wasLaidOff],
        }
      ),
      choice(
        'internal',
        'Ask if there are other positions available',
        'HR found an opening in another department. A lateral move with a small pay cut, but at least you still have a job.',
        { effects: { stats: { happiness: -5, stress: 5 } } }
      ),
      choice(
        'accept',
        'Accept it gracefully',
        'You shook hands, packed your desk, and left with dignity. The standard severance will help.',
        {
          effects: {
            stats: { happiness: -15, stress: 20 },
            resources: { money: 1000 },
          },
          setFlags: [CAREER_FLAGS.wasLaidOff],
        }
      ),
      choice(
        'fight',
        "Fight it - this isn't fair",
        'You argued your case, but the decision was made above your manager. A smaller severance and a cold goodbye.',
        {
          energyCost: 10,
          effects: {
            stats: { happiness: -15, stress: 30 },
            resources: { money: 750 },
          },
          setFlags: [CAREER_FLAGS.wasLaidOff],
        }
      ),
    ],
  },

  // 4b. Layoff -> Job Search (MULTI-STAGE, stage 2; choice-gated PASSIVE).
  // Only fires for players who actually left (set `wasLaidOff`); the internal
  // transfer branch does not. Delay approximated via a 14-day cooldown proxy
  // after the layoff fired (cooldown keyed off layoffNotice would not apply, so
  // we use the flag + a maxAge-unbounded passive that fires once).
  {
    id: 'layoffJobSearch',
    category: 'career',
    kind: 'passive',
    prompt: 'The Job Search',
    minAge: 22,
    maxAge: 67,
    weight: 1,
    isEligible: (player) =>
      wasAsked(player, 'layoffNotice') && playerHasFlag(player, CAREER_FLAGS.wasLaidOff),
    choices: [
      choice(
        'searching',
        'Continue',
        'A couple of weeks after the layoff, you are deep in applications. The rejection emails sting, but a few leads look promising. Stay strong.',
        { effects: { stats: { stress: 5 } } }
      ),
    ],
  },

  // ===========================================================================
  // 5. Competitor Counter-Offer (INDEPENDENT, once)
  // Legacy: jump (sure), leverage (55% raise roll), stay. Flatten: leverage
  // yields the successful-match outcome (representative path).
  // ===========================================================================
  {
    id: 'competitorCounterOffer',
    category: 'career',
    prompt:
      'A recruiter from a competitor reaches out: "We would love to have you - we are offering 30% more than you make now." What do you do?',
    minAge: 25,
    maxAge: 55,
    weight: 1,
    isEligible: (player) => hasOccupation(player, 'work'),
    choices: [
      choice(
        'jump',
        'Take the new job',
        'You accepted and put in your two weeks. Fresh start, fresh challenges, and a bigger paycheck.',
        { effects: { stats: { happiness: 5, stress: 15 }, resources: { money: 650 } } }
      ),
      choice(
        'leverage',
        'Use it to negotiate a raise here',
        'You told your manager about the offer. After some back-and-forth, they matched it. "We don\'t want to lose you."',
        { energyCost: 5, effects: { stats: { happiness: 5 }, resources: { money: 450 } } }
      ),
      choice(
        'stay',
        "Decline - I'm happy where I am",
        'You politely declined the recruiter. Sometimes stability is worth more than a bigger paycheck.',
        { effects: { stats: { happiness: 2 } } }
      ),
    ],
  },

  // ===========================================================================
  // 6. Start Own Business (MULTI-STAGE, stage 1)
  // Legacy: all-in cost $10k, set ownsBusiness, 40% success roll scheduling a
  // success/struggle follow-up; side-hustle scheduled a progress follow-up.
  // Flatten: all-in costs money + sets `startedBusiness` flag; side-hustle sets
  // `businessSideHustle`. Stage 2 (outcome) is a SEPARATE passive event gated on
  // the relevant flag. The success/struggle gamble collapses to the
  // representative "turning a profit" outcome.
  // ===========================================================================
  {
    id: 'startOwnBusiness',
    category: 'career',
    prompt:
      'You have had a business idea brewing for months. The numbers look promising, but pursuing it means quitting your stable job and investing your savings. Are you ready to take the leap?',
    minAge: 25,
    maxAge: 55,
    weight: 1,
    isEligible: (player) =>
      hasOccupation(player, 'work') &&
      typeof player.c.money === 'number' &&
      (player.c.money as number) >= 5000,
    choices: [
      choice(
        'allin',
        'Quit and go all in ($10,000)',
        'You handed in your resignation and invested $10,000 into your dream. No safety net - terrifying and exhilarating.',
        {
          energyCost: 10,
          moneyCost: 10000,
          effects: { stats: { happiness: 5, stress: 20 } },
          setFlags: [CAREER_FLAGS.startedBusiness],
        }
      ),
      choice(
        'side',
        'Start it as a side hustle first',
        'You decided to build it on the side. Nights and weekends are now dedicated to your business while keeping a paycheck.',
        {
          energyCost: 15,
          effects: { stats: { stress: 10 } },
          setFlags: [CAREER_FLAGS.businessSideHustle],
        }
      ),
      choice(
        'stay',
        'Too risky - keep the day job',
        'You shelved the idea for now. There is nothing wrong with a steady paycheck. Maybe someday.',
        { effects: { stats: { stress: -3 } } }
      ),
    ],
  },

  // 6b. Business Outcome (MULTI-STAGE, stage 2; choice-gated PASSIVE).
  // Fires only for the all-in founders (set `startedBusiness`). The legacy
  // 40%-success gamble is flattened to the representative profit outcome.
  {
    id: 'businessOutcome',
    category: 'career',
    kind: 'passive',
    prompt: 'Business Outcome',
    minAge: 25,
    maxAge: 60,
    weight: 1,
    isEligible: (player) =>
      wasAsked(player, 'startOwnBusiness') && playerHasFlag(player, CAREER_FLAGS.startedBusiness),
    choices: [
      choice(
        'profit',
        'Continue',
        'Your business is turning a profit! After two grueling months, customers are coming in and revenue is growing. The risk is paying off.',
        { effects: { stats: { happiness: 12, stress: -5 }, resources: { money: 800 } } }
      ),
    ],
  },

  // 6c. Side-Hustle Progress (MULTI-STAGE, stage 2; choice-gated PASSIVE).
  // Fires only for the side-hustle path (set `businessSideHustle`).
  {
    id: 'sideHustleProgress',
    category: 'career',
    kind: 'passive',
    prompt: 'Side Hustle Progress',
    minAge: 25,
    maxAge: 60,
    weight: 1,
    isEligible: (player) =>
      wasAsked(player, 'startOwnBusiness') && playerHasFlag(player, CAREER_FLAGS.businessSideHustle),
    choices: [
      choice(
        'modest',
        'Continue',
        'Your side hustle is making a little money - real, but not enough to quit yet. The question is whether you can sustain both.',
        { effects: { stats: { stress: 3 }, resources: { money: 200 } } }
      ),
    ],
  },

  // ===========================================================================
  // 7. Workplace Conflict (INDEPENDENT, once)
  // Legacy: each branch had random good/bad outcomes + scheduled follow-ups.
  // Flatten: representative outcomes per branch.
  // ===========================================================================
  {
    id: 'workplaceConflict',
    category: 'career',
    prompt:
      'A coworker took credit for your work in a big presentation. The disrespect stings. How do you handle it?',
    minAge: 20,
    maxAge: 65,
    weight: 1,
    isEligible: (player) => hasOccupation(player, 'work'),
    choices: [
      choice(
        'private',
        'Talk to them privately',
        'The private conversation went better than expected. They apologized, you set boundaries, and the air feels cleared.',
        { energyCost: 5, effects: { stats: { happiness: 3, social: 3 } } }
      ),
      choice(
        'hr',
        'Escalate to HR',
        'You filed a formal complaint. HR investigated and the coworker got a warning. Awkward, but there is a paper trail now.',
        { energyCost: 3, effects: { stats: { stress: 5 } } }
      ),
      choice(
        'ignore',
        'Let it go this time',
        'You decided to focus on your work, but every time you see them the resentment builds a little more.',
        { effects: { stats: { happiness: -5, stress: 8 } } }
      ),
      choice(
        'public',
        'Confront them publicly',
        'You called them out in the meeting. The room went silent, then your manager backed you up. They got the message.',
        { energyCost: 8, effects: { stats: { happiness: 5 } } }
      ),
    ],
  },

  // ===========================================================================
  // 8. Flagship Project (INTELLIGENCE-GATED payoff, T007)
  // High-traffic recurring career opportunity that ONLY appears for a sharp
  // mind (intelligence >= FLAGSHIP_PROJECT_INTELLIGENCE). This is the career
  // payoff for studying: leadership hands the most capable people the
  // career-defining work, with an outsized reward to match. Repeatable with a
  // long cooldown so it recurs across a long career without spamming. Gating
  // BONUS content (never a milestone), so it cannot make anything unreachable.
  // ===========================================================================
  {
    id: 'flagshipProject',
    category: 'career',
    prompt:
      'Leadership pulls you aside: a high-visibility flagship project needs someone who can actually carry it. They picked you. It is a lot of pressure, but the kind that makes careers.',
    minAge: 24,
    maxAge: 62,
    weight: 2,
    repeatable: true,
    cooldownDays: 540,
    isEligible: (player) =>
      hasOccupation(player, 'work') && stat(player, 'intelligence') >= FLAGSHIP_PROJECT_INTELLIGENCE,
    choices: [
      choice(
        'lead_it',
        'Lead it and deliver',
        'You owned the project end to end and shipped it. It became the thing people point to when they talk about you. A standout bonus and a reputation to match.',
        {
          energyCost: 15,
          effects: { stats: { happiness: 14, stress: 10, prestige: 8 }, resources: { money: 1200 } },
        }
      ),
      choice(
        'co_lead',
        'Build a team and share the load',
        'You assembled the right people and steered the ship together. The project succeeded, and you earned a reputation as someone who lifts everyone around them.',
        {
          energyCost: 8,
          effects: { stats: { happiness: 10, social: 8, prestige: 5 }, resources: { money: 700 } },
        }
      ),
      choice(
        'pass',
        'Pass - the timing is wrong',
        'You declined gracefully, citing your current load. Sensible, but you wonder if you just let a defining moment pass by.',
        { effects: { stats: { stress: -4, happiness: -2 } } }
      ),
    ],
  },
];

// Date-seeded text variation for the REPEATABLE performance review so the
// yearly review does not read identically every time. Seeded on age/day so the
// copy is stable within a single firing but varies across years. Applied via
// the optional `*Fn` override; the static text remains the fallback.
function reviewSeed(player: EventPlayerContext, salt: string): string {
  const day =
    (player as { dayOfYear?: unknown }).dayOfYear ??
    (player as { date?: unknown }).date ??
    0;
  return `perfReview:${salt}:${player.c.ageYears ?? 0}:${String(day)}`;
}

function reviewSeeded(salt: string, variants: string[]): DynamicTextFn {
  return (player) => pickVariant(variants, reviewSeed(player, salt));
}

const reviewPrompts = [
  'It is time for your annual performance review. Your manager has scheduled a one-on-one. How do you prepare?',
  'Review season is here. Your manager wants to sit down and go over the past year. How do you get ready?',
  'Your annual performance review is on the calendar. The whole team is a little tense. How do you prepare?',
  "It's that time of year again - the dreaded (or welcomed) performance review. How do you approach it?",
];

const reviewPreparedResolutions = [
  'You walked in prepared and your manager said you "exceed expectations." You earned a raise and recognition.',
  'Your binder of accomplishments spoke for itself. "Exceeds expectations" - and a raise to match.',
  'You came armed with receipts. Your manager was impressed, the review glowed, and a raise followed.',
];

const reviewFeedbackResolutions = [
  'Solid review. "Meets expectations" across the board, with a couple of areas to grow in.',
  'A fair, steady review. You meet the bar, with a few notes on where to push next year.',
  'No surprises - a solid "meets expectations" and some constructive pointers to chew on.',
];

const reviewWingResolutions = [
  'The review did not go well. "Needs improvement" in several areas, and a 30-day improvement plan. A wake-up call.',
  'Winging it backfired. "Needs improvement," an awkward silence, and a plan you did not want. A wake-up call.',
  'It showed that you did not prepare. The feedback stung, and now there is a performance plan to climb out of.',
];

{
  const review = careerCatalog.find((e) => e.id === 'performanceReview');
  if (review) {
    review.promptFn = reviewSeeded('prompt', reviewPrompts);
    const prepared = review.choices.find((c) => c.choiceId === 'prepared');
    const feedback = review.choices.find((c) => c.choiceId === 'feedback');
    const wing = review.choices.find((c) => c.choiceId === 'wing');
    if (prepared) prepared.resolutionTextFn = reviewSeeded('prepared', reviewPreparedResolutions);
    if (feedback) feedback.resolutionTextFn = reviewSeeded('feedback', reviewFeedbackResolutions);
    if (wing) wing.resolutionTextFn = reviewSeeded('wing', reviewWingResolutions);
  }
}

/**
 * Deterministic resolution-text lookup for a career choice (mirrors the
 * dilemma catalog helper, used by tests and any caller needing the resolved
 * copy without firing the engine).
 */
export function resolveCareerChoice(eventId: string, choiceId: string): string | null {
  const definition = careerCatalog.find((event) => event.id === eventId);
  if (!definition) {
    return null;
  }
  const selectedChoice = definition.choices.find((item) => item.choiceId === choiceId);
  return selectedChoice?.resolutionText ?? null;
}
