import type { IEntity } from '@core/shared/domain';
import { IdentityDomainValidationError, IdentityDomainInvariantError } from '../errors/IdentityDomainError';

export interface AdminVote {
  voterId: string;
  positive: boolean;
  votedAt: Date;
}

export interface AdminVoteOutcome {
  percentPositive: number;
  count: {
    positive: number;
    negative: number;
    total: number;
  };
  eligibleVoterCount: number;
  participationRate: number;
  outcome: 'positive' | 'negative' | 'tie';
}

export interface AdminVoteSessionProps {
  voteSessionId: string;
  leagueId: string;
  adminId: string;
  startDate: Date;
  endDate: Date;
  eligibleVoters: string[]; // User IDs
  votes?: AdminVote[];
  closed?: boolean;
  outcome?: AdminVoteOutcome;
  createdAt?: Date;
  updatedAt?: Date;
}

/**
 * AdminVoteSession Entity
 * 
 * Aggregate root for admin vote sessions scoped to a league.
 * Controls who can vote, deduplication, time windows, and closure.
 * Emits outcome events that convert to rating ledger events.
 * 
 * Based on ratings-architecture-concept.md sections 5.2.1 and 7.1.1
 */
export class AdminVoteSession implements IEntity<string> {
  readonly id: string;
  readonly leagueId: string;
  readonly adminId: string;
  readonly startDate: Date;
  readonly endDate: Date;
  readonly eligibleVoters: string[];
  private _votes: AdminVote[];
  private _closed: boolean;
  private _outcome: AdminVoteOutcome | undefined;
  readonly createdAt: Date;
  private _updatedAt: Date;

  private constructor(props: AdminVoteSessionProps) {
    this.id = props.voteSessionId;
    this.leagueId = props.leagueId;
    this.adminId = props.adminId;
    this.startDate = props.startDate;
    this.endDate = props.endDate;
    this.eligibleVoters = props.eligibleVoters;
    this._votes = props.votes || [];
    this._closed = props.closed || false;
    this._outcome = props.outcome;
    this.createdAt = props.createdAt || new Date();
    this._updatedAt = props.updatedAt || new Date();
  }

  static create(props: AdminVoteSessionProps): AdminVoteSession {
    // Validate required fields
    if (!props.voteSessionId || props.voteSessionId.trim().length === 0) {
      throw new IdentityDomainValidationError('voteSessionId is required');
    }

    if (!props.leagueId || props.leagueId.trim().length === 0) {
      throw new IdentityDomainValidationError('leagueId is required');
    }

    if (!props.adminId || props.adminId.trim().length === 0) {
      throw new IdentityDomainValidationError('adminId is required');
    }

    if (!props.startDate || !props.endDate) {
      throw new IdentityDomainValidationError('startDate and endDate are required');
    }

    if (props.startDate >= props.endDate) {
      throw new IdentityDomainInvariantError('startDate must be before endDate');
    }

    if (!props.eligibleVoters || props.eligibleVoters.length === 0) {
      throw new IdentityDomainValidationError('At least one eligible voter is required');
    }

    // Validate no duplicate eligible voters
    const uniqueVoters = new Set(props.eligibleVoters);
    if (uniqueVoters.size !== props.eligibleVoters.length) {
      throw new IdentityDomainInvariantError('Duplicate eligible voters are not allowed');
    }

    // Validate votes if provided
    if (props.votes) {
      const voterIds = new Set<string>();
      for (const vote of props.votes) {
        if (!vote.voterId || vote.voterId.trim().length === 0) {
          throw new IdentityDomainValidationError('Vote voterId is required');
        }
        if (!props.eligibleVoters.includes(vote.voterId)) {
          throw new IdentityDomainInvariantError(`Voter ${vote.voterId} is not eligible`);
        }
        if (voterIds.has(vote.voterId)) {
          throw new IdentityDomainInvariantError(`Duplicate vote from voter ${vote.voterId}`);
        }
        if (!vote.votedAt) {
          throw new IdentityDomainValidationError('Vote timestamp is required');
        }
        voterIds.add(vote.voterId);
      }
    }

    // Validate outcome if provided
    if (props.outcome) {
      if (props.outcome.percentPositive < 0 || props.outcome.percentPositive > 100) {
        throw new IdentityDomainValidationError('percentPositive must be between 0 and 100');
      }
      if (props.outcome.eligibleVoterCount !== props.eligibleVoters.length) {
        throw new IdentityDomainInvariantError('eligibleVoterCount must match eligibleVoters length');
      }
    }

    return new AdminVoteSession(props);
  }

  static rehydrate(props: AdminVoteSessionProps): AdminVoteSession {
    // Rehydration assumes data is already validated (from persistence)
    return new AdminVoteSession(props);
  }

  // Getters
  get votes(): AdminVote[] {
    return [...this._votes];
  }

  get closed(): boolean {
    return this._closed;
  }

  get outcome(): AdminVoteOutcome | undefined {
    return this._outcome;
  }

  get updatedAt(): Date {
    return this._updatedAt;
  }

  /**
   * Cast a vote in this session
   * @param voterId - The user ID of the voter
   * @param positive - Whether the vote is positive (true) or negative (false)
   * @param votedAt - When the vote was cast (optional, defaults to now)
   * @throws Error if session is closed, voter is not eligible, or already voted
   */
  castVote(voterId: string, positive: boolean, votedAt: Date = new Date()): void {
    if (this._closed) {
      throw new IdentityDomainInvariantError('Cannot cast vote: session is closed');
    }

    if (!this.eligibleVoters.includes(voterId)) {
      throw new IdentityDomainInvariantError(`Voter ${voterId} is not eligible for this session`);
    }

    if (this._votes.some(v => v.voterId === voterId)) {
      throw new IdentityDomainInvariantError(`Voter ${voterId} has already voted`);
    }

    if (votedAt < this.startDate || votedAt > this.endDate) {
      throw new IdentityDomainInvariantError('Vote timestamp is outside the voting window');
    }

    this._votes.push({
      voterId,
      positive,
      votedAt,
    });

    this._updatedAt = new Date();
  }

  /**
   * Close the vote session and calculate outcome
   * @throws Error if session is already closed
   * @returns The calculated outcome
   */
  close(): AdminVoteOutcome {
    if (this._closed) {
      throw new IdentityDomainInvariantError('Session is already closed');
    }

    const now = new Date();
    if (now < this.startDate || now > this.endDate) {
      throw new IdentityDomainInvariantError('Cannot close session outside the voting window');
    }

    const positiveVotes = this._votes.filter(v => v.positive).length;
    const negativeVotes = this._votes.filter(v => !v.positive).length;
    const totalVotes = this._votes.length;
    const eligibleVoterCount = this.eligibleVoters.length;

    const percentPositive = totalVotes > 0 ? (positiveVotes / totalVotes) * 100 : 0;
    const participationRate = (totalVotes / eligibleVoterCount) * 100;

    let outcome: 'positive' | 'negative' | 'tie';
    if (totalVotes === 0) {
      outcome = 'tie';
    } else if (positiveVotes > negativeVotes) {
      outcome = 'positive';
    } else if (negativeVotes > positiveVotes) {
      outcome = 'negative';
    } else {
      outcome = 'tie';
    }

    this._outcome = {
      percentPositive: Math.round(percentPositive * 100) / 100, // Round to 2 decimal places
      count: {
        positive: positiveVotes,
        negative: negativeVotes,
        total: totalVotes,
      },
      eligibleVoterCount,
      participationRate: Math.round(participationRate * 100) / 100,
      outcome,
    };

    this._closed = true;
    this._updatedAt = now;

    return this._outcome;
  }

  /**
   * Check if a voter has already voted
   */
  hasVoted(voterId: string): boolean {
    return this._votes.some(v => v.voterId === voterId);
  }

  /**
   * Get vote by voter ID
   */
  getVote(voterId: string): AdminVote | undefined {
    return this._votes.find(v => v.voterId === voterId);
  }

  /**
   * Get count of votes cast
   */
  getVoteCount(): number {
    return this._votes.length;
  }

  /**
   * Check if session is within voting window
   */
  isVotingWindowOpen(now: Date = new Date()): boolean {
    return now >= this.startDate && now <= this.endDate && !this._closed;
  }

  equals(other: IEntity<string>): boolean {
    return this.id === other.id;
  }

  toJSON(): Record<string, unknown> {
    return {
      voteSessionId: this.id,
      leagueId: this.leagueId,
      adminId: this.adminId,
      startDate: this.startDate.toISOString(),
      endDate: this.endDate.toISOString(),
      eligibleVoters: this.eligibleVoters,
      votes: this._votes.map(v => ({
        voterId: v.voterId,
        positive: v.positive,
        votedAt: v.votedAt.toISOString(),
      })),
      closed: this._closed,
      outcome: this._outcome,
      createdAt: this.createdAt.toISOString(),
      updatedAt: this._updatedAt.toISOString(),
    };
  }
}
