/**
 * SessionState Entity
 *
 * Tracks the state of an MPC session from this party's perspective.
 * Used for monitoring and recovery purposes.
 */

import { v4 as uuidv4 } from 'uuid';
import { SessionId, PartyId, PublicKey, MessageHash, Signature } from '../value-objects';
import { SessionType, SessionStatus, ParticipantStatus } from '../enums';
import {
  DomainEvent,
  PartyJoinedSessionEvent,
  KeygenCompletedEvent,
  SigningCompletedEvent,
  SessionFailedEvent,
  SessionTimeoutEvent,
} from '../events';

export interface Participant {
  partyId: string;
  partyIndex: number;
  status: ParticipantStatus;
}

export interface SessionStateCreateParams {
  sessionId: SessionId;
  partyId: PartyId;
  partyIndex: number;
  sessionType: SessionType;
  participants: Participant[];
  thresholdN: number;
  thresholdT: number;
  publicKey?: PublicKey;
  messageHash?: MessageHash;
}

export interface SessionStateReconstructParams extends SessionStateCreateParams {
  id: string;
  status: SessionStatus;
  currentRound: number;
  errorMessage?: string;
  signature?: Signature;
  startedAt: Date;
  completedAt?: Date;
}

export class SessionState {
  // ============================================================================
  // Private Fields
  // ============================================================================
  private readonly _id: string;
  private readonly _sessionId: SessionId;
  private readonly _partyId: PartyId;
  private readonly _partyIndex: number;
  private readonly _sessionType: SessionType;
  private readonly _participants: Participant[];
  private readonly _thresholdN: number;
  private readonly _thresholdT: number;
  private _status: SessionStatus;
  private _currentRound: number;
  private _errorMessage?: string;
  private _publicKey?: PublicKey;
  private _messageHash?: MessageHash;
  private _signature?: Signature;
  private readonly _startedAt: Date;
  private _completedAt?: Date;

  // Domain events collection
  private readonly _domainEvents: DomainEvent[] = [];

  // ============================================================================
  // Constructor (private - use factory methods)
  // ============================================================================
  private constructor(
    id: string,
    sessionId: SessionId,
    partyId: PartyId,
    partyIndex: number,
    sessionType: SessionType,
    participants: Participant[],
    thresholdN: number,
    thresholdT: number,
    status: SessionStatus,
    currentRound: number,
    startedAt: Date,
    errorMessage?: string,
    publicKey?: PublicKey,
    messageHash?: MessageHash,
    signature?: Signature,
    completedAt?: Date,
  ) {
    this._id = id;
    this._sessionId = sessionId;
    this._partyId = partyId;
    this._partyIndex = partyIndex;
    this._sessionType = sessionType;
    this._participants = participants;
    this._thresholdN = thresholdN;
    this._thresholdT = thresholdT;
    this._status = status;
    this._currentRound = currentRound;
    this._startedAt = startedAt;
    this._errorMessage = errorMessage;
    this._publicKey = publicKey;
    this._messageHash = messageHash;
    this._signature = signature;
    this._completedAt = completedAt;
  }

  // ============================================================================
  // Factory Methods
  // ============================================================================

  /**
   * Create a new session state when joining a session
   */
  static create(params: SessionStateCreateParams): SessionState {
    const session = new SessionState(
      uuidv4(),
      params.sessionId,
      params.partyId,
      params.partyIndex,
      params.sessionType,
      params.participants,
      params.thresholdN,
      params.thresholdT,
      SessionStatus.IN_PROGRESS,
      0,
      new Date(),
      undefined,
      params.publicKey,
      params.messageHash,
    );

    // Emit joined event
    session.addDomainEvent(new PartyJoinedSessionEvent(
      params.sessionId.value,
      params.partyId.value,
      params.partyIndex,
      params.sessionType,
    ));

    return session;
  }

  /**
   * Reconstruct from persistence
   */
  static reconstruct(params: SessionStateReconstructParams): SessionState {
    return new SessionState(
      params.id,
      params.sessionId,
      params.partyId,
      params.partyIndex,
      params.sessionType,
      params.participants,
      params.thresholdN,
      params.thresholdT,
      params.status,
      params.currentRound,
      params.startedAt,
      params.errorMessage,
      params.publicKey,
      params.messageHash,
      params.signature,
      params.completedAt,
    );
  }

  // ============================================================================
  // Getters
  // ============================================================================
  get id(): string {
    return this._id;
  }

  get sessionId(): SessionId {
    return this._sessionId;
  }

  get partyId(): PartyId {
    return this._partyId;
  }

  get partyIndex(): number {
    return this._partyIndex;
  }

  get sessionType(): SessionType {
    return this._sessionType;
  }

  get participants(): Participant[] {
    return [...this._participants];
  }

  get thresholdN(): number {
    return this._thresholdN;
  }

  get thresholdT(): number {
    return this._thresholdT;
  }

  get status(): SessionStatus {
    return this._status;
  }

  get currentRound(): number {
    return this._currentRound;
  }

  get errorMessage(): string | undefined {
    return this._errorMessage;
  }

  get publicKey(): PublicKey | undefined {
    return this._publicKey;
  }

  get messageHash(): MessageHash | undefined {
    return this._messageHash;
  }

  get signature(): Signature | undefined {
    return this._signature;
  }

  get startedAt(): Date {
    return this._startedAt;
  }

  get completedAt(): Date | undefined {
    return this._completedAt;
  }

  get domainEvents(): DomainEvent[] {
    return [...this._domainEvents];
  }

  // ============================================================================
  // Business Methods
  // ============================================================================

  /**
   * Update the current round number
   */
  advanceRound(roundNumber: number): void {
    this.ensureInProgress();
    if (roundNumber <= this._currentRound) {
      throw new Error(`Cannot go back to round ${roundNumber} from ${this._currentRound}`);
    }
    this._currentRound = roundNumber;
  }

  /**
   * Mark keygen as completed
   */
  completeKeygen(publicKey: PublicKey, shareId: string): void {
    this.ensureInProgress();
    if (this._sessionType !== SessionType.KEYGEN) {
      throw new Error('Cannot complete keygen for non-keygen session');
    }

    this._publicKey = publicKey;
    this._status = SessionStatus.COMPLETED;
    this._completedAt = new Date();

    this.addDomainEvent(new KeygenCompletedEvent(
      this._sessionId.value,
      this._partyId.value,
      publicKey.toHex(),
      shareId,
      `${this._thresholdT}-of-${this._thresholdN}`,
    ));
  }

  /**
   * Mark signing as completed
   */
  completeSigning(signature: Signature): void {
    this.ensureInProgress();
    if (this._sessionType !== SessionType.SIGN) {
      throw new Error('Cannot complete signing for non-signing session');
    }
    if (!this._messageHash) {
      throw new Error('Message hash not set for signing session');
    }
    if (!this._publicKey) {
      throw new Error('Public key not set for signing session');
    }

    this._signature = signature;
    this._status = SessionStatus.COMPLETED;
    this._completedAt = new Date();

    this.addDomainEvent(new SigningCompletedEvent(
      this._sessionId.value,
      this._partyId.value,
      this._messageHash.toHex(),
      signature.toHex(),
      this._publicKey.toHex(),
    ));
  }

  /**
   * Mark session as failed
   */
  fail(errorMessage: string, errorCode?: string): void {
    if (this._status === SessionStatus.COMPLETED) {
      throw new Error('Cannot fail a completed session');
    }

    this._status = SessionStatus.FAILED;
    this._errorMessage = errorMessage;
    this._completedAt = new Date();

    this.addDomainEvent(new SessionFailedEvent(
      this._sessionId.value,
      this._partyId.value,
      this._sessionType,
      errorMessage,
      errorCode,
    ));
  }

  /**
   * Mark session as timed out
   */
  timeout(): void {
    if (this._status === SessionStatus.COMPLETED) {
      throw new Error('Cannot timeout a completed session');
    }

    this._status = SessionStatus.TIMEOUT;
    this._completedAt = new Date();

    this.addDomainEvent(new SessionTimeoutEvent(
      this._sessionId.value,
      this._partyId.value,
      this._sessionType,
      this._currentRound,
    ));
  }

  /**
   * Update participant status
   */
  updateParticipantStatus(partyId: string, status: ParticipantStatus): void {
    const participant = this._participants.find(p => p.partyId === partyId);
    if (!participant) {
      throw new Error(`Participant ${partyId} not found`);
    }
    participant.status = status;
  }

  /**
   * Check if session is in progress
   */
  isInProgress(): boolean {
    return this._status === SessionStatus.IN_PROGRESS;
  }

  /**
   * Check if session is completed
   */
  isCompleted(): boolean {
    return this._status === SessionStatus.COMPLETED;
  }

  /**
   * Get the duration of the session in milliseconds
   */
  getDuration(): number | undefined {
    if (!this._completedAt) {
      return Date.now() - this._startedAt.getTime();
    }
    return this._completedAt.getTime() - this._startedAt.getTime();
  }

  // ============================================================================
  // Domain Event Management
  // ============================================================================

  addDomainEvent(event: DomainEvent): void {
    this._domainEvents.push(event);
  }

  clearDomainEvents(): void {
    this._domainEvents.length = 0;
  }

  // ============================================================================
  // Private Helper Methods
  // ============================================================================

  private ensureInProgress(): void {
    if (this._status !== SessionStatus.IN_PROGRESS) {
      throw new Error(`Cannot perform operation on ${this._status} session`);
    }
  }
}
