import { Driver } from '@core/racing/domain/entities/Driver';
import { JoinRequest } from '@core/racing/domain/entities/JoinRequest';
import { League } from '@core/racing/domain/entities/League';
import { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership';
import { Race } from '@core/racing/domain/entities/Race';
import { RaceRegistration } from '@core/racing/domain/entities/RaceRegistration';
import { seedId } from './SeedIdHelper';

export class RacingMembershipFactory {
  constructor(
    private readonly baseDate: Date,
    private readonly persistence: 'postgres' | 'inmemory' = 'inmemory',
  ) {}

  createLeagueMemberships(drivers: Driver[], leagues: League[]): LeagueMembership[] {
    const memberships: LeagueMembership[] = [];
    const leagueById = new Map(leagues.map(l => [l.id.toString(), l]));

    const add = (props: {
      leagueId: string;
      driverId: string;
      role: 'owner' | 'admin' | 'steward' | 'member';
      status: 'active' | 'inactive' | 'pending';
      joinedDaysAgo: number;
      id?: string;
    }): void => {
      memberships.push(
        LeagueMembership.create({
          leagueId: props.leagueId,
          driverId: props.driverId,
          role: props.role,
          status: props.status,
          joinedAt: this.addDays(this.baseDate, -props.joinedDaysAgo),
          ...(props.id !== undefined ? { id: props.id } : {}),
        }),
      );
    };

    // Empty league: intentionally no memberships.
    // (Keep `league-2` empty if it exists.)
    // Widen the type to avoid TS2367 "no overlap" comparisons in some build modes.
    const emptyLeagueId: string | undefined = leagueById.has(seedId('league-2', this.persistence)) ? (seedId('league-2', this.persistence) as string) : undefined;

    // Demo league: "full" + overbooked with pending/inactive members.
    const demoLeague = leagueById.get(seedId('league-5', this.persistence));
    if (demoLeague) {
      const maxDrivers = demoLeague.settings.maxDrivers ?? 32;
      const activeDrivers = drivers.slice(0, Math.min(maxDrivers, drivers.length));

      activeDrivers.forEach((driver, idx) => {
        const driverId = driver.id.toString();
        const expectedDriverId = seedId('driver-1', this.persistence);
        const role =
          driverId === expectedDriverId
            ? 'owner'
            : idx === 1 || idx === 2
              ? 'admin'
              : idx === 3 || idx === 4
                ? 'steward'
                : 'member';

        add({ leagueId: demoLeague.id.toString(), driverId, role, status: 'active', joinedDaysAgo: 60 - idx });
      });

      // Over-cap edge cases (membership exists but not active / pending)
      const overbooked = drivers.slice(activeDrivers.length, activeDrivers.length + 4);
      overbooked.forEach((driver, idx) => {
        add({
          leagueId: demoLeague.id.toString(),
          driverId: driver.id.toString(),
          role: 'member',
          status: idx % 2 === 0 ? 'pending' : 'inactive',
          joinedDaysAgo: 10 + idx,
        });
      });
    }

    // League with mixed statuses and roles (but not full).
    const league1 = leagueById.get(seedId('league-1', this.persistence));
    if (league1) {
      const pick = drivers.slice(15, 25);
      pick.forEach((driver, idx) => {
        add({
          leagueId: league1.id.toString(),
          driverId: driver.id.toString(),
          role: idx === 0 ? 'owner' : idx === 1 ? 'steward' : 'member',
          status: idx % 5 === 0 ? 'pending' : idx % 7 === 0 ? 'inactive' : 'active',
          joinedDaysAgo: 30 + idx,
        });
      });
    }

    // League with only pending memberships (tests "pending list" UX).
    const league4 = leagueById.get(seedId('league-4', this.persistence));
    if (league4) {
      drivers.slice(40, 48).forEach((driver, idx) => {
        add({
          leagueId: league4.id.toString(),
          driverId: driver.id.toString(),
          role: idx === 0 ? 'owner' : 'member',
          status: 'pending',
          joinedDaysAgo: 3 + idx,
        });
      });
    }

    // Spread remaining drivers across remaining leagues to create realistic overlap.
    // IMPORTANT: in postgres mode league ids are UUIDs, so never parse numeric suffixes from ids.
    for (const driver of drivers) {
      const driverId = driver.id.toString();

      for (const league of leagues) {
        const leagueId = league.id.toString();
        if (leagueId === seedId('league-5', this.persistence)) continue;
        if (emptyLeagueId && leagueId === emptyLeagueId) continue;

        // Deterministic membership distribution that works with UUID ids.
        // Use stable hash of (driverId + leagueId) rather than parsing numeric suffixes.
        const distKey = `${driverId}:${leagueId}`;
        const dist = this.stableHash(distKey);

        // Some inactive memberships for league-3 to exercise edge cases.
        if (leagueId === seedId('league-3', this.persistence) && dist % 11 === 0) {
          add({
            leagueId,
            driverId,
            role: 'member',
            status: 'inactive',
            joinedDaysAgo: 120,
          });
          continue;
        }

        // Sparse membership distribution (not every driver in every league)
        if (dist % 9 === 0) {
          add({
            leagueId,
            driverId,
            role: 'member',
            status: 'active',
            joinedDaysAgo: 45,
          });
        }
      }
    }

    return memberships;
  }

  createLeagueJoinRequests(
    drivers: Driver[],
    leagues: League[],
    leagueMemberships: LeagueMembership[],
  ): JoinRequest[] {
    const membershipIds = new Set<string>(leagueMemberships.map(m => m.id.toString()));
    const requests: JoinRequest[] = [];

    const addRequest = (input: { leagueId: string; driverId: string; id?: string; message?: string; requestedAt?: Date }) => {
      requests.push(
        JoinRequest.create({
          leagueId: input.leagueId,
          driverId: input.driverId,
          ...(input.id !== undefined && { id: input.id }),
          ...(input.message !== undefined && { message: input.message }),
          ...(input.requestedAt !== undefined && { requestedAt: input.requestedAt }),
        }),
      );
    };

    // League with lots of requests + membership/request conflicts (everyone is a member of league-5 already).
    const demoLeagueId = seedId('league-5', this.persistence);
    const demoDrivers = drivers.slice(10, 35);
    demoDrivers.forEach((driver, idx) => {
      const message =
        idx % 4 === 0
          ? 'Interested in consistent stewarding and clean racing.'
          : idx % 4 === 1
            ? undefined
            : idx % 4 === 2
              ? ''
              : 'Can I join mid-season and still be eligible for points?';

      addRequest({
        leagueId: demoLeagueId,
        driverId: driver.id.toString(),
        requestedAt: this.addDays(this.baseDate, -(7 + idx)),
        ...(message !== undefined && { message }),
      });
    });

    // League with a few "normal" requests (only drivers who are NOT members already).
    const targetLeagueId = seedId('league-1', this.persistence);
    const nonMembers = drivers
      .filter(driver => !membershipIds.has(`${targetLeagueId}:${driver.id.toString()}`))
      .slice(0, 6);

    nonMembers.forEach((driver, idx) => {
      addRequest({
        leagueId: targetLeagueId,
        driverId: driver.id.toString(),
        requestedAt: this.addDays(this.baseDate, -(3 + idx)),
        ...(idx % 2 === 0 && { message: 'Looking for regular endurance rounds and stable race times.' }),
      });
    });

    // Single request with no message (explicit id).
    const league3Exists = leagues.some(l => l.id.toString() === seedId('league-3', this.persistence));
    if (league3Exists && drivers[0]) {
      addRequest({
        id: seedId('league-3-join-req-1', this.persistence),
        leagueId: seedId('league-3', this.persistence),
        driverId: drivers[0].id.toString(),
        requestedAt: this.addDays(this.baseDate, -9),
      });
    }

    // Duplicate id edge case (last write wins in in-memory repo).
    if (drivers[1]) {
      addRequest({
        id: seedId('dup-league-join-req-1', this.persistence),
        leagueId: seedId('league-7', this.persistence),
        driverId: drivers[1].id.toString(),
        requestedAt: this.addDays(this.baseDate, -2),
        message: 'First request message (will be overwritten).',
      });

      addRequest({
        id: seedId('dup-league-join-req-1', this.persistence),
        leagueId: seedId('league-7', this.persistence),
        driverId: drivers[1].id.toString(),
        requestedAt: this.addDays(this.baseDate, -1),
        message: 'Updated request message (duplicate id).',
      });
    }

    // Explicit conflict: join request exists even though membership exists.
    const expectedDriverId = seedId('driver-1', this.persistence);
    const driver1 = drivers.find(d => d.id.toString() === expectedDriverId);
    if (driver1) {
      addRequest({
        id: seedId('conflict-req-league-5-driver-1', this.persistence),
        leagueId: demoLeagueId,
        driverId: driver1.id.toString(),
        requestedAt: this.addDays(this.baseDate, -15),
        message: 'Testing UI edge case: request exists for an existing member.',
      });
    }

    return requests;
  }

  createRaceRegistrations(
    races: Race[],
    drivers: Driver[],
    leagueMemberships: LeagueMembership[],
  ): RaceRegistration[] {
    const registrations: RaceRegistration[] = [];
    const activeMembershipKey = new Set(
      leagueMemberships
        .filter(m => m.status.toString() === 'active')
        .map(m => `${m.leagueId.toString()}:${m.driverId.toString()}`),
    );

    const scheduled = races.filter((r) => r.status.toString() === 'scheduled');

    for (const race of scheduled) {
      const leagueId = race.leagueId.toString();
      const targetCount = (race as unknown as { registeredCount?: number }).registeredCount ?? 0;

      // 25%: intentionally no registrations
      if (Number(race.id.toString().split('-')[1] ?? 0) % 4 === 0) {
        continue;
      }

      const eligibleDrivers = drivers
        .map(d => d.id.toString())
        .filter(driverId => activeMembershipKey.has(`${leagueId}:${driverId}`));

      const desired = Math.min(
        eligibleDrivers.length,
        Math.max(1, targetCount > 0 ? targetCount : 3),
      );

      const start = Number(race.id.toString().split('-')[1] ?? 0);
      for (let i = 0; i < desired; i++) {
        const driverId = eligibleDrivers[(start + i) % eligibleDrivers.length];
        if (!driverId) continue;

        registrations.push(
          RaceRegistration.create({
            raceId: race.id,
            driverId,
          }),
        );
      }

      // Edge case: one "outsider" registration (driver not active in league)
      if (eligibleDrivers.length > 0 && drivers.length > eligibleDrivers.length) {
        const outsider = drivers
          .map(d => d.id.toString())
          .find(driverId => !activeMembershipKey.has(`${leagueId}:${driverId}`));

        if (outsider && start % 7 === 0) {
          registrations.push(
            RaceRegistration.create({
              raceId: race.id,
              driverId: outsider,
            }),
          );
        }
      }

      // Edge case: duplicate registration (should be ignored by repo if unique constrained)
      if (start % 9 === 0 && registrations.length > 0) {
        const last = registrations[registrations.length - 1]!;
        registrations.push(
          RaceRegistration.create({
            raceId: last.raceId.toString(),
            driverId: last.driverId.toString(),
          }),
        );
      }
    }

    // Keep a tiny curated "happy path" for the demo league as well
    const upcomingDemoLeague = races.filter((r) => r.status.toString() === 'scheduled' && r.leagueId === seedId('league-5', this.persistence)).slice(0, 3);
    for (const race of upcomingDemoLeague) {
      registrations.push(
        RaceRegistration.create({
          raceId: race.id,
          driverId: seedId('driver-1', this.persistence),
        }),
      );
    }

    return registrations;
  }

  private stableHash(input: string): number {
    // Simple deterministic string hash (non-crypto), stable across runs.
    let hash = 0;
    for (let i = 0; i < input.length; i++) {
      hash = (hash * 31 + input.charCodeAt(i)) | 0;
    }
    return Math.abs(hash);
  }

  private addDays(date: Date, days: number): Date {
    return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
  }
}