/**
 * Domain Entity: Penalty
 *
 * Represents a penalty applied to a driver for an incident during a race.
 * Penalties can be applied as a result of an upheld protest or directly by stewards.
 */

import { Entity } from '@core/shared/domain/Entity';
import { RacingDomainInvariantError, RacingDomainValidationError } from '../../errors/RacingDomainError';
import { AppliedAt } from '../AppliedAt';
import { DriverId } from '../DriverId';
import { IssuedAt } from '../IssuedAt';
import { LeagueId } from '../LeagueId';
import { ProtestId } from '../ProtestId';
import { RaceId } from '../RaceId';
import { StewardId } from '../StewardId';
import { PenaltyId } from './PenaltyId';
import { PenaltyNotes } from './PenaltyNotes';
import { PenaltyReason } from './PenaltyReason';
import { PenaltyStatus } from './PenaltyStatus';
import { PenaltyType, type PenaltyTypeValue } from './PenaltyType';
import { PenaltyValue } from './PenaltyValue';

export interface PenaltyProps {
  id: PenaltyId;
  leagueId: LeagueId;
  raceId: RaceId;
  /** The driver receiving the penalty */
  driverId: DriverId;
  /** Type of penalty */
  type: PenaltyType;
  /** Value depends on type: seconds for time_penalty, positions for grid_penalty, points for points_deduction */
  value?: PenaltyValue;
  /** Reason for the penalty */
  reason: PenaltyReason;
  /** ID of the protest that led to this penalty (if applicable) */
  protestId?: ProtestId;
  /** ID of the steward who issued the penalty */
  issuedBy: StewardId;
  /** Current status of the penalty */
  status: PenaltyStatus;
  /** Timestamp when the penalty was issued */
  issuedAt: IssuedAt;
  /** Timestamp when the penalty was applied to results */
  appliedAt?: AppliedAt;
  /** Notes about the penalty application */
  notes?: PenaltyNotes;
}

export const PENALTY_TYPES_REQUIRING_VALUE: PenaltyTypeValue[] = [
  'time_penalty',
  'grid_penalty',
  'points_deduction',
  'license_points',
  'fine',
  'race_ban',
];

export function penaltyTypeRequiresValue(type: PenaltyTypeValue): boolean {
  return PENALTY_TYPES_REQUIRING_VALUE.includes(type);
}

export class Penalty extends Entity<PenaltyId> {
  private constructor(private readonly props: PenaltyProps) {
    super(props.id);}

  static create(props: {
    id: string;
    leagueId: string;
    raceId: string;
    driverId: string;
    type: string;
    value?: number;
    reason: string;
    protestId?: string;
    issuedBy: string;
    status?: string;
    issuedAt?: Date;
    appliedAt?: Date;
    notes?: string;
  }): Penalty {
    if (!props.id) throw new RacingDomainValidationError('Penalty ID is required');
    if (!props.leagueId) throw new RacingDomainValidationError('League ID is required');
    if (!props.raceId) throw new RacingDomainValidationError('Race ID is required');
    if (!props.driverId) throw new RacingDomainValidationError('Driver ID is required');
    if (!props.type) throw new RacingDomainValidationError('Penalty type is required');
    if (!props.reason?.trim()) throw new RacingDomainValidationError('Penalty reason is required');
    if (!props.issuedBy) throw new RacingDomainValidationError('Penalty must be issued by a steward');

    const penaltyType = PenaltyType.create(props.type);

    // Validate value based on type
    if (penaltyTypeRequiresValue(penaltyType.toString())) {
      if (props.value === undefined || props.value <= 0) {
        throw new RacingDomainValidationError(`${penaltyType.toString()} requires a positive value`);
      }
    }

    const penaltyProps: PenaltyProps = {
      id: PenaltyId.create(props.id),
      leagueId: LeagueId.create(props.leagueId),
      raceId: RaceId.create(props.raceId),
      driverId: DriverId.create(props.driverId),
      type: penaltyType,
      reason: PenaltyReason.create(props.reason),
      issuedBy: StewardId.create(props.issuedBy),
      status: PenaltyStatus.create(props.status || 'pending'),
      issuedAt: IssuedAt.create(props.issuedAt || new Date()),
      ...(props.value !== undefined && { value: PenaltyValue.create(props.value) }),
      ...(props.protestId !== undefined && { protestId: ProtestId.create(props.protestId) }),
      ...(props.appliedAt !== undefined && { appliedAt: AppliedAt.create(props.appliedAt) }),
      ...(props.notes !== undefined && { notes: PenaltyNotes.create(props.notes) }),
    };

    return new Penalty(penaltyProps);
  }

  static rehydrate(props: {
    id: string;
    leagueId: string;
    raceId: string;
    driverId: string;
    type: string;
    value?: number;
    reason: string;
    protestId?: string;
    issuedBy: string;
    status: string;
    issuedAt: Date;
    appliedAt?: Date;
    notes?: string;
  }): Penalty {
    const penaltyType = PenaltyType.create(props.type);

    if (penaltyTypeRequiresValue(penaltyType.toString())) {
      if (props.value === undefined || props.value <= 0) {
        throw new RacingDomainValidationError(`${penaltyType.toString()} requires a positive value`);
      }
    }

    return new Penalty({
      id: PenaltyId.create(props.id),
      leagueId: LeagueId.create(props.leagueId),
      raceId: RaceId.create(props.raceId),
      driverId: DriverId.create(props.driverId),
      type: penaltyType,
      ...(props.value !== undefined ? { value: PenaltyValue.create(props.value) } : {}),
      reason: PenaltyReason.create(props.reason),
      ...(props.protestId !== undefined ? { protestId: ProtestId.create(props.protestId) } : {}),
      issuedBy: StewardId.create(props.issuedBy),
      status: PenaltyStatus.create(props.status),
      issuedAt: IssuedAt.create(props.issuedAt),
      ...(props.appliedAt !== undefined ? { appliedAt: AppliedAt.create(props.appliedAt) } : {}),
      ...(props.notes !== undefined ? { notes: PenaltyNotes.create(props.notes) } : {}),
    });
  }

  get leagueId(): string { return this.props.leagueId.toString(); }
  get raceId(): string { return this.props.raceId.toString(); }
  get driverId(): string { return this.props.driverId.toString(); }
  get type(): string { return this.props.type.toString(); }
  get value(): number | undefined { return this.props.value?.toNumber(); }
  get reason(): string { return this.props.reason.toString(); }
  get protestId(): string | undefined { return this.props.protestId?.toString(); }
  get issuedBy(): string { return this.props.issuedBy.toString(); }
  get status(): string { return this.props.status.toString(); }
  get issuedAt(): Date { return this.props.issuedAt.toDate(); }
  get appliedAt(): Date | undefined { return this.props.appliedAt?.toDate(); }
  get notes(): string | undefined { return this.props.notes?.toString(); }

  isPending(): boolean {
    return this.props.status.toString() === 'pending';
  }

  isApplied(): boolean {
    return this.props.status.toString() === 'applied';
  }

  /**
   * Mark penalty as applied (after recalculating results)
   */
  markAsApplied(notes?: string): Penalty {
    if (this.isApplied()) {
      throw new RacingDomainInvariantError('Penalty is already applied');
    }
    if (this.props.status.toString() === 'overturned') {
      throw new RacingDomainInvariantError('Cannot apply an overturned penalty');
    }
    const base: PenaltyProps = {
      ...this.props,
      status: PenaltyStatus.create('applied'),
      appliedAt: AppliedAt.create(new Date()),
    };

    const next: PenaltyProps =
      notes !== undefined ? { ...base, notes: PenaltyNotes.create(notes) } : base;

    return new Penalty(next);
  }

  /**
   * Overturn the penalty (e.g., after successful appeal)
   */
  overturn(reason: string): Penalty {
    if (this.props.status.toString() === 'overturned') {
      throw new RacingDomainInvariantError('Penalty is already overturned');
    }
    return new Penalty({
      ...this.props,
      status: PenaltyStatus.create('overturned'),
      notes: PenaltyNotes.create(reason),
    });
  }

  /**
   * Get a human-readable description of the penalty
   */
  getDescription(): string {
    switch (this.props.type.toString()) {
      case 'time_penalty':
        return `+${this.props.value?.toNumber()}s time penalty`;
      case 'grid_penalty':
        return `${this.props.value?.toNumber()} place grid penalty (next race)`;
      case 'points_deduction':
        return `${this.props.value?.toNumber()} championship points deducted`;
      case 'disqualification':
        return 'Disqualified from race';
      case 'warning':
        return 'Official warning';
      case 'license_points':
        return `${this.props.value?.toNumber()} license penalty points`;
      case 'probation':
        return 'Probationary period';
      case 'fine':
        return `${this.props.value?.toNumber()} points fine`;
      case 'race_ban':
        return `${this.props.value?.toNumber()} race suspension`;
      default:
        return 'Penalty';
    }
  }

  equals(other?: Entity<PenaltyId>): boolean {
    if (!other || !(other instanceof Penalty)) {
      return false;
    }
    return this.id.equals(other.id);
  }
}
