import { Penalty } from '@core/racing/domain/entities/penalty/Penalty';
import { Protest } from '@core/racing/domain/entities/Protest';

import { TypeOrmPersistenceSchemaError } from '../errors/TypeOrmPersistenceSchemaError';
import { PenaltyOrmEntity, ProtestOrmEntity } from '../entities/MissingRacingOrmEntities';
import {
  assertDate,
  assertEnumValue,
  assertNonEmptyString,
  assertOptionalInteger,
  assertOptionalStringOrNull,
  assertRecord,
} from '../schema/TypeOrmSchemaGuards';

const VALID_PROTEST_STATUSES = [
  'pending',
  'awaiting_defense',
  'under_review',
  'upheld',
  'dismissed',
  'withdrawn',
] as const;

const VALID_PENALTY_STATUSES = ['pending', 'applied', 'appealed', 'overturned'] as const;

const VALID_PENALTY_TYPES = [
  'time_penalty',
  'grid_penalty',
  'points_deduction',
  'disqualification',
  'warning',
  'license_points',
  'probation',
  'fine',
  'race_ban',
] as const;

type SerializedProtestIncident = {
  lap: number;
  description: string;
  timeInRace?: number;
};

type SerializedProtestDefense = {
  statement: string;
  submittedAt: string;
  videoUrl?: string;
};

function assertSerializedProtestIncident(value: unknown): asserts value is SerializedProtestIncident {
  const entityName = 'Protest';
  const fieldName = 'incident';

  assertRecord(entityName, fieldName, value);

  const lap = value.lap;
  if (typeof lap !== 'number' || !Number.isFinite(lap)) {
    throw new TypeOrmPersistenceSchemaError({ entityName, fieldName: `${fieldName}.lap`, reason: 'not_number' });
  }

  const description = value.description;
  if (typeof description !== 'string') {
    throw new TypeOrmPersistenceSchemaError({ entityName, fieldName: `${fieldName}.description`, reason: 'not_string' });
  }
  if (description.trim().length === 0) {
    throw new TypeOrmPersistenceSchemaError({
      entityName,
      fieldName: `${fieldName}.description`,
      reason: 'empty_string',
    });
  }

  if (value.timeInRace !== undefined) {
    const timeInRace = value.timeInRace;
    if (typeof timeInRace !== 'number' || !Number.isFinite(timeInRace)) {
      throw new TypeOrmPersistenceSchemaError({
        entityName,
        fieldName: `${fieldName}.timeInRace`,
        reason: 'not_number',
      });
    }
  }
}

function assertSerializedProtestDefense(value: unknown): asserts value is SerializedProtestDefense {
  const entityName = 'Protest';
  const fieldName = 'defense';

  assertRecord(entityName, fieldName, value);

  const statement = value.statement;
  if (typeof statement !== 'string') {
    throw new TypeOrmPersistenceSchemaError({ entityName, fieldName: `${fieldName}.statement`, reason: 'not_string' });
  }
  if (statement.trim().length === 0) {
    throw new TypeOrmPersistenceSchemaError({
      entityName,
      fieldName: `${fieldName}.statement`,
      reason: 'empty_string',
    });
  }

  const submittedAt = value.submittedAt;
  if (typeof submittedAt !== 'string') {
    throw new TypeOrmPersistenceSchemaError({
      entityName,
      fieldName: `${fieldName}.submittedAt`,
      reason: 'not_string',
    });
  }

  const submittedAtDate = new Date(submittedAt);
  if (Number.isNaN(submittedAtDate.getTime()) || submittedAtDate.toISOString() !== submittedAt) {
    throw new TypeOrmPersistenceSchemaError({
      entityName,
      fieldName: `${fieldName}.submittedAt`,
      reason: 'not_iso_date',
    });
  }

  if (value.videoUrl !== undefined && typeof value.videoUrl !== 'string') {
    throw new TypeOrmPersistenceSchemaError({ entityName, fieldName: `${fieldName}.videoUrl`, reason: 'not_string' });
  }
}

function serializeProtestDefense(defense: Protest['defense']): SerializedProtestDefense | undefined {
  if (!defense) return undefined;

  return {
    statement: defense.statement.toString(),
    submittedAt: defense.submittedAt.toDate().toISOString(),
    ...(defense.videoUrl !== undefined ? { videoUrl: defense.videoUrl.toString() } : {}),
  };
}

export class PenaltyOrmMapper {
  toOrmEntity(domain: Penalty): PenaltyOrmEntity {
    const entity = new PenaltyOrmEntity();
    entity.id = domain.id;
    entity.leagueId = domain.leagueId;
    entity.raceId = domain.raceId;
    entity.driverId = domain.driverId;
    entity.type = domain.type;
    entity.value = domain.value ?? null;
    entity.reason = domain.reason;
    entity.protestId = domain.protestId ?? null;
    entity.issuedBy = domain.issuedBy;
    entity.status = domain.status;
    entity.issuedAt = domain.issuedAt;
    entity.appliedAt = domain.appliedAt ?? null;
    entity.notes = domain.notes ?? null;
    return entity;
  }

  toDomain(entity: PenaltyOrmEntity): Penalty {
    const entityName = 'Penalty';

    assertNonEmptyString(entityName, 'id', entity.id);
    assertNonEmptyString(entityName, 'leagueId', entity.leagueId);
    assertNonEmptyString(entityName, 'raceId', entity.raceId);
    assertNonEmptyString(entityName, 'driverId', entity.driverId);
    assertEnumValue(entityName, 'type', entity.type, VALID_PENALTY_TYPES);
    assertOptionalInteger(entityName, 'value', entity.value);
    assertNonEmptyString(entityName, 'reason', entity.reason);
    assertOptionalStringOrNull(entityName, 'protestId', entity.protestId);
    assertNonEmptyString(entityName, 'issuedBy', entity.issuedBy);
    assertEnumValue(entityName, 'status', entity.status, VALID_PENALTY_STATUSES);
    assertDate(entityName, 'issuedAt', entity.issuedAt);
    assertOptionalStringOrNull(entityName, 'notes', entity.notes);

    if (entity.appliedAt !== null && entity.appliedAt !== undefined) {
      assertDate(entityName, 'appliedAt', entity.appliedAt);
    }

    try {
      return Penalty.rehydrate({
        id: entity.id,
        leagueId: entity.leagueId,
        raceId: entity.raceId,
        driverId: entity.driverId,
        type: entity.type,
        ...(entity.value !== null && entity.value !== undefined ? { value: entity.value } : {}),
        reason: entity.reason,
        ...(entity.protestId !== null && entity.protestId !== undefined ? { protestId: entity.protestId } : {}),
        issuedBy: entity.issuedBy,
        status: entity.status,
        issuedAt: entity.issuedAt,
        ...(entity.appliedAt !== null && entity.appliedAt !== undefined ? { appliedAt: entity.appliedAt } : {}),
        ...(entity.notes !== null && entity.notes !== undefined ? { notes: entity.notes } : {}),
      });
    } catch {
      throw new TypeOrmPersistenceSchemaError({ entityName, fieldName: '__root', reason: 'invalid_shape' });
    }
  }
}

export class ProtestOrmMapper {
  toOrmEntity(domain: Protest): ProtestOrmEntity {
    const entity = new ProtestOrmEntity();
    entity.id = domain.id;
    entity.raceId = domain.raceId;
    entity.protestingDriverId = domain.protestingDriverId;
    entity.accusedDriverId = domain.accusedDriverId;
    entity.incident = {
      lap: domain.incident.lap.toNumber(),
      description: domain.incident.description.toString(),
      ...(domain.incident.timeInRace !== undefined ? { timeInRace: domain.incident.timeInRace.toNumber() } : {}),
    };
    entity.comment = domain.comment ?? null;
    entity.proofVideoUrl = domain.proofVideoUrl ?? null;
    entity.status = domain.status.toString();
    entity.reviewedBy = domain.reviewedBy ?? null;
    entity.decisionNotes = domain.decisionNotes ?? null;
    entity.filedAt = domain.filedAt;
    entity.reviewedAt = domain.reviewedAt ?? null;
    entity.defense = serializeProtestDefense(domain.defense) ?? null;
    entity.defenseRequestedAt = domain.defenseRequestedAt ?? null;
    entity.defenseRequestedBy = domain.defenseRequestedBy ?? null;
    return entity;
  }

  toDomain(entity: ProtestOrmEntity): Protest {
    const entityName = 'Protest';

    assertNonEmptyString(entityName, 'id', entity.id);
    assertNonEmptyString(entityName, 'raceId', entity.raceId);
    assertNonEmptyString(entityName, 'protestingDriverId', entity.protestingDriverId);
    assertNonEmptyString(entityName, 'accusedDriverId', entity.accusedDriverId);
    assertEnumValue(entityName, 'status', entity.status, VALID_PROTEST_STATUSES);
    assertDate(entityName, 'filedAt', entity.filedAt);

    assertSerializedProtestIncident(entity.incident);

    assertOptionalStringOrNull(entityName, 'comment', entity.comment);
    assertOptionalStringOrNull(entityName, 'proofVideoUrl', entity.proofVideoUrl);
    assertOptionalStringOrNull(entityName, 'decisionNotes', entity.decisionNotes);

    if (entity.reviewedAt !== null && entity.reviewedAt !== undefined) {
      assertDate(entityName, 'reviewedAt', entity.reviewedAt);
    }
    if (entity.defenseRequestedAt !== null && entity.defenseRequestedAt !== undefined) {
      assertDate(entityName, 'defenseRequestedAt', entity.defenseRequestedAt);
    }

    if (entity.reviewedBy !== null && entity.reviewedBy !== undefined) {
      assertNonEmptyString(entityName, 'reviewedBy', entity.reviewedBy);
    }
    if (entity.defenseRequestedBy !== null && entity.defenseRequestedBy !== undefined) {
      assertNonEmptyString(entityName, 'defenseRequestedBy', entity.defenseRequestedBy);
    }

    let defense: { statement: string; submittedAt: Date; videoUrl?: string } | undefined;
    if (entity.defense !== null && entity.defense !== undefined) {
      assertSerializedProtestDefense(entity.defense);
      const parsed = entity.defense as SerializedProtestDefense;
      defense = {
        statement: parsed.statement,
        submittedAt: new Date(parsed.submittedAt),
        ...(parsed.videoUrl !== undefined ? { videoUrl: parsed.videoUrl } : {}),
      };
    }

    try {
      return Protest.rehydrate({
        id: entity.id,
        raceId: entity.raceId,
        protestingDriverId: entity.protestingDriverId,
        accusedDriverId: entity.accusedDriverId,
        incident: {
          lap: (entity.incident as SerializedProtestIncident).lap,
          description: (entity.incident as SerializedProtestIncident).description,
          ...((entity.incident as SerializedProtestIncident).timeInRace !== undefined
            ? { timeInRace: (entity.incident as SerializedProtestIncident).timeInRace }
            : {}),
        },
        ...(entity.comment !== null && entity.comment !== undefined ? { comment: entity.comment } : {}),
        ...(entity.proofVideoUrl !== null && entity.proofVideoUrl !== undefined ? { proofVideoUrl: entity.proofVideoUrl } : {}),
        status: entity.status,
        ...(entity.reviewedBy !== null && entity.reviewedBy !== undefined ? { reviewedBy: entity.reviewedBy } : {}),
        ...(entity.decisionNotes !== null && entity.decisionNotes !== undefined ? { decisionNotes: entity.decisionNotes } : {}),
        filedAt: entity.filedAt,
        ...(entity.reviewedAt !== null && entity.reviewedAt !== undefined ? { reviewedAt: entity.reviewedAt } : {}),
        ...(defense !== undefined ? { defense } : {}),
        ...(entity.defenseRequestedAt !== null && entity.defenseRequestedAt !== undefined
          ? { defenseRequestedAt: entity.defenseRequestedAt }
          : {}),
        ...(entity.defenseRequestedBy !== null && entity.defenseRequestedBy !== undefined
          ? { defenseRequestedBy: entity.defenseRequestedBy }
          : {}),
      });
    } catch {
      throw new TypeOrmPersistenceSchemaError({ entityName, fieldName: '__root', reason: 'invalid_shape' });
    }
  }
}