import { readFileSync, existsSync } from 'node:fs';
import { canonicalTeam } from '../../src/lib/teams';
import { computeGroupTables } from '../../src/lib/standings';
import type { Fixture, FixturesFile, MatchStatus, Snapshot, Stage } from '../../src/lib/types';

/** A normalized live match from any source (football-data, sofascore, dev). */
export interface LiveMatch {
  home: string;
  away: string;
  group: string | null;
  stage: Stage;
  /** ISO kickoff if the source provides it (helps match knockout fixtures). */
  kickoff: string | null;
  homeScore: number | null;
  awayScore: number | null;
  status: MatchStatus;
  minute: number | null;
}

export type Source = Snapshot['source'];

/** Candidate locations for the generated fixtures.json (prod dist, then dev public). */
function loadFixturesFile(staticDir: string): FixturesFile {
  const candidates = [
    `${staticDir}/data/fixtures.json`,
    `${process.cwd()}/public/data/fixtures.json`,
    `${process.cwd()}/dist/data/fixtures.json`,
  ];
  for (const p of candidates) {
    if (existsSync(p)) return JSON.parse(readFileSync(p, 'utf8')) as FixturesFile;
  }
  throw new Error(`fixtures.json not found. Run "npm run data:fixtures". Looked in: ${candidates.join(', ')}`);
}

const utcDate = (iso: string): string => iso.slice(0, 10);

/** In-memory tournament: the static seed plus any live overlay. */
export class TournamentState {
  readonly season: string;
  private fixtures: Fixture[];
  private byNum: Map<number, Fixture>;
  updatedAt: string | null = null;
  source: Source = 'seed';

  constructor(staticDir: string) {
    const file = loadFixturesFile(staticDir);
    this.season = file.season;
    this.fixtures = file.fixtures;
    this.byNum = new Map(this.fixtures.map((f) => [f.num, f]));
  }

  snapshot(): Snapshot {
    return {
      season: this.season,
      updatedAt: this.updatedAt,
      source: this.source,
      fixtures: this.fixtures,
      tables: computeGroupTables(this.fixtures),
    };
  }

  getFixture(num: number): Fixture | undefined {
    return this.byNum.get(num);
  }

  /** The live fixtures array (for the model engine). */
  allFixtures(): Fixture[] {
    return this.fixtures;
  }

  /** Which fixture number does a (home, away) pairing map to? Used by the
   *  ingestion layer to key provider event ids against our fixtures. */
  matchFixtureNum(home: string, away: string, stage: Fixture['stage'] = 'group', kickoff: string | null = null): number | null {
    const f = this.findFixture({
      home, away, group: null, stage, kickoff,
      homeScore: null, awayScore: null, status: 'scheduled', minute: null,
    });
    return f?.num ?? null;
  }

  /** True if any match is live or kicks off within `windowMs` of now — used to
   *  poll the APIs often during match windows and rarely when nothing is on. */
  hasLiveActivity(windowMs: number): boolean {
    const now = Date.now();
    return this.fixtures.some(
      (f) => f.status === 'live' || Math.abs(new Date(f.kickoff).getTime() - now) <= windowMs,
    );
  }

  /** Directly set a fixture's score/status (used by the dev injection endpoint). */
  applyScore(
    num: number,
    homeScore: number | null,
    awayScore: number | null,
    status: MatchStatus,
    minute: number | null,
    source: Source,
  ): Fixture | null {
    const f = this.byNum.get(num);
    if (!f) return null;
    f.homeScore = homeScore;
    f.awayScore = awayScore;
    f.status = status;
    f.minute = minute;
    this.updatedAt = new Date().toISOString();
    this.source = source;
    return f;
  }

  /**
   * Merge a batch of live matches onto the seed. Group matches match by group +
   * unordered team pair; knockout matches by kickoff date + team overlap. Returns
   * the fixtures that actually changed.
   */
  mergeLive(matches: LiveMatch[], source: Source): Fixture[] {
    const changed: Fixture[] = [];
    for (const lm of matches) {
      const f = this.findFixture(lm);
      if (!f) continue;
      if (this.applyLive(f, lm)) changed.push(f);
    }
    if (changed.length) {
      this.updatedAt = new Date().toISOString();
      this.source = source;
    }
    return changed;
  }

  private findFixture(lm: LiveMatch): Fixture | undefined {
    const home = canonicalTeam(lm.home);
    const away = canonicalTeam(lm.away);

    // 1) Strongest signal: an unordered match of two known team names. Works for
    //    every group game and any already-resolved knockout, regardless of the
    //    source's stage hint (SofaScore's is unreliable).
    const byPair = this.fixtures.find(
      (f) =>
        f.home.team && f.away.team &&
        ((f.home.team === home && f.away.team === away) ||
          (f.home.team === away && f.away.team === home)),
    );
    if (byPair) return byPair;

    // 2) Knockout slot not yet filled in the seed: match by stage + kickoff date,
    //    preferring a fixture whose slots are still unresolved.
    if (lm.stage !== 'group') {
      const sameStage = this.fixtures.filter(
        (f) => f.stage === lm.stage && (lm.kickoff ? utcDate(f.kickoff) === utcDate(lm.kickoff) : true),
      );
      if (sameStage.length === 1) return sameStage[0];
      return sameStage.find((f) => !f.home.team || !f.away.team) ?? sameStage[0];
    }
    return undefined;
  }

  /** Overlay a live match onto a fixture, orienting scores to the seed's home/away. */
  private applyLive(f: Fixture, lm: LiveMatch): boolean {
    const home = canonicalTeam(lm.home);
    const away = canonicalTeam(lm.away);

    // Fill concrete teams for resolved knockout slots.
    if (!f.home.team && !f.away.team) {
      f.home = { team: home, placeholder: f.home.placeholder, label: home };
      f.away = { team: away, placeholder: f.away.placeholder, label: away };
    }

    const swapped = f.home.team === away && f.away.team === home;
    const hs = swapped ? lm.awayScore : lm.homeScore;
    const as = swapped ? lm.homeScore : lm.awayScore;

    const before = `${f.status}:${f.homeScore}:${f.awayScore}:${f.minute}`;
    f.homeScore = hs;
    f.awayScore = as;
    f.status = lm.status;
    f.minute = lm.minute;
    const after = `${f.status}:${f.homeScore}:${f.awayScore}:${f.minute}`;
    return before !== after;
  }
}
