/**
 * Merge Logic for collaborative screenplay editing
 * Handles complex merge scenarios and screenplay-specific rules
 */

import { Doc, Text } from 'yjs';
import { ChangeTracker, DocumentChange } from './change-tracker';

export type MergeStrategy = 'accept-local' | 'accept-remote' | 'manual' | 'auto-merge';

export type ConflictEventType = 'conflict-detected' | 'conflict-resolved' | 'merge-complete';

export interface ConflictEvent {
  type: ConflictEventType;
  conflict?: Conflict;
  result?: MergeResult;
}

export type ConflictListener = (event: ConflictEvent) => void;

export interface MergeResult {
  success: boolean;
  strategy: MergeStrategy;
  conflicts: Conflict[];
  appliedChanges: DocumentChange[];
}

export interface Conflict {
  id: string;
  type: 'concurrent-edit' | 'format-conflict' | 'structure-conflict';
  localChange: DocumentChange;
  remoteChange: DocumentChange;
  resolution?: Resolution;
}

export interface Resolution {
  strategy: MergeStrategy;
  result: 'local' | 'remote' | 'merged';
  resolvedAt: Date;
  resolvedBy: string;
}

export interface ServerChange {
  id: string;
  userId: string;
  timestamp: Date;
  type: 'insert' | 'delete' | 'format';
  position: number;
  content?: string;
  length: number;
}

export class MergeLogic {
  private doc: Doc;
  private userId: string;
  private pendingConflicts: Conflict[] = [];
  private changeTracker: ChangeTracker | null = null;
  private conflictListeners: Set<ConflictListener> = new Set();

  constructor(doc: Doc, userId: string, changeTracker?: ChangeTracker) {
    this.doc = doc;
    this.userId = userId;
    this.changeTracker = changeTracker ?? null;
  }

  /**
   * Set or update the ChangeTracker reference
   */
  setChangeTracker(tracker: ChangeTracker): void {
    this.changeTracker = tracker;
  }

  /**
   * Listen for conflict events
   */
  onConflict(listener: ConflictListener): void {
    this.conflictListeners.add(listener);
  }

  /**
   * Remove conflict listener
   */
  removeConflictListener(listener: ConflictListener): void {
    this.conflictListeners.delete(listener);
  }

  /**
   * Emit a conflict event to all listeners
   */
  private emit(event: ConflictEvent): void {
    this.conflictListeners.forEach(listener => listener(event));
  }

  /**
   * Apply a server change to the local document
   */
  applyServerChange(change: ServerChange): MergeResult {
    const conflicts: Conflict[] = [];
    const appliedChanges: DocumentChange[] = [];

    try {
      this.doc.transact(() => {
        const text = this.doc.getText('main');
        
        // Check for conflicts with local changes
        const hasConflict = this.detectConflict(change);
        
        if (hasConflict) {
          const localChange = this.getLastLocalChange();
          if (!localChange) {
            // No local change to conflict with, apply remote change
            this.applyChange(text, change);
            return;
          }
          
          const conflict: Conflict = {
            id: this.generateConflictId(),
            type: 'concurrent-edit',
            localChange,
            remoteChange: this.convertServerToChange(change),
          };
          
          conflicts.push(conflict);
          this.pendingConflicts.push(conflict);
          
          // Emit conflict detected event
          this.emit({ type: 'conflict-detected', conflict });
          
          // Auto-resolve simple conflicts
          const resolution = this.autoResolveConflict(conflict);
          if (resolution) {
            conflict.resolution = resolution;
            
            if (resolution.result === 'local') {
              // Keep local change, ignore remote
              this.emit({ type: 'conflict-resolved', conflict });
              return;
            } else if (resolution.result === 'remote') {
              // Apply remote change
              this.applyChange(text, change);
              this.emit({ type: 'conflict-resolved', conflict });
            } else {
              // Merged - apply both
              this.applyChange(text, change);
              this.emit({ type: 'conflict-resolved', conflict });
            }
          }
        } else {
          // No conflict, apply change directly
          this.applyChange(text, change);
        }
      }, 'server-change');

      const result: MergeResult = {
        success: conflicts.length === 0,
        strategy: conflicts.length > 0 ? 'auto-merge' : 'accept-remote',
        conflicts,
        appliedChanges,
      };

      this.emit({ type: 'merge-complete', result });

      return result;
    } catch (error) {
      console.error('Failed to apply server change:', error);
      return {
        success: false,
        strategy: 'manual',
        conflicts,
        appliedChanges,
      };
    }
  }

  /**
   * Apply a change to the text document
   */
  private applyChange(text: Text, change: ServerChange): void {
    switch (change.type) {
      case 'insert':
        if (change.content) {
          text.insert(change.position, change.content);
        }
        break;
      case 'delete':
        text.delete(change.position, change.length);
        break;
      case 'format':
        // Format changes would be handled separately
        break;
    }
  }

  /**
   * Detect if a server change conflicts with local changes
   */
  private detectConflict(change: ServerChange): boolean {
    // Simplified conflict detection
    // In production, would check against pending local changes
    const lastChange = this.getLastLocalChange();
    
    if (!lastChange) {
      return false;
    }

    // Check if positions overlap
    const positionOverlap = 
      change.position >= lastChange.position &&
      change.position < lastChange.position + lastChange.length;

    return positionOverlap;
  }

  /**
   * Get the last local change
   */
  private getLastLocalChange(): DocumentChange | null {
    if (!this.changeTracker) {
      return null;
    }
    const changes = this.changeTracker.getAllChanges();
    // Find the most recent change made by this user
    for (let i = changes.length - 1; i >= 0; i--) {
      const change = changes[i];
      if (change && change.userId === this.userId) {
        return change;
      }
    }
    return null;
  }

  /**
   * Convert server change to DocumentChange format
   */
  private convertServerToChange(serverChange: ServerChange): DocumentChange {
    return {
      id: serverChange.id,
      userId: serverChange.userId,
      userName: 'Remote User',
      timestamp: serverChange.timestamp,
      type: serverChange.type,
      position: serverChange.position,
      length: serverChange.length,
      content: serverChange.content,
      accepted: true,
    };
  }

  /**
   * Auto-resolve simple conflicts
   */
  private autoResolveConflict(conflict: Conflict): Resolution | null {
    // Auto-resolve non-overlapping edits
    if (conflict.type === 'concurrent-edit') {
      const local = conflict.localChange;
      const remote = conflict.remoteChange;

      // If edits are far apart, no conflict
      const distance = Math.abs(local.position - remote.position);
      if (distance > 10) {
        return {
          strategy: 'auto-merge',
          result: 'merged',
          resolvedAt: new Date(),
          resolvedBy: 'auto',
        };
      }

      // If same user made both changes, accept remote
      if (local.userId === remote.userId) {
        return {
          strategy: 'accept-remote',
          result: 'remote',
          resolvedAt: new Date(),
          resolvedBy: 'auto',
        };
      }
    }

    // Can't auto-resolve, needs manual intervention
    return null;
  }

  /**
   * Handle concurrent edits to the same region
   */
  handleConcurrentEdit(localChange: DocumentChange, remoteChange: DocumentChange): MergeStrategy {
    // Screenplay-specific merge rules
    
    // Rule 1: If both changes are in the same scene, prefer structured edits
    if (this.sameScene(localChange, remoteChange)) {
      // If one is formatting and one is content, accept both
      if (localChange.type !== remoteChange.type) {
        return 'auto-merge';
      }
      
      // If both are content edits, need manual resolution
      return 'manual';
    }

    // Rule 2: If changes are in different scenes, auto-merge
    return 'auto-merge';
  }

  /**
   * Check if two changes are in the same scene
   */
  private sameScene(change1: DocumentChange, change2: DocumentChange): boolean {
    // In production, would check scene boundaries in the document
    // For now, assume changes within 500 chars are in the same scene
    return Math.abs(change1.position - change2.position) < 500;
  }

  /**
   * Resolve a conflict manually
   */
  resolveConflict(conflict: Conflict, strategy: MergeStrategy, resolverId: string): boolean {
    const resolution: Resolution = {
      strategy,
      result: strategy === 'accept-local' ? 'local' : strategy === 'accept-remote' ? 'remote' : 'merged',
      resolvedAt: new Date(),
      resolvedBy: resolverId,
    };

    conflict.resolution = resolution;

    // Remove from pending conflicts
    const index = this.pendingConflicts.indexOf(conflict);
    if (index > -1) {
      this.pendingConflicts.splice(index, 1);
    }

    // Apply resolution
    if (resolution.result === 'remote') {
      const text = this.doc.getText('main');
      this.applyChange(text, {
        id: conflict.remoteChange.id,
        userId: conflict.remoteChange.userId,
        timestamp: conflict.remoteChange.timestamp,
        type: conflict.remoteChange.type as 'insert' | 'delete' | 'format',
        position: conflict.remoteChange.position,
        length: conflict.remoteChange.length,
        content: conflict.remoteChange.content,
      });
    }

    return true;
  }

  /**
   * Validate a merge result
   */
  validateMerge(result: MergeResult): boolean {
    // Check document integrity
    try {
      const text = this.doc.getText('main');
      const content = text.toString();
      
      // Basic validation: document should not be empty
      if (content.length === 0) {
        return false;
      }

      // Check for corrupted UTF-8 sequences
      try {
        new TextDecoder().decode(new TextEncoder().encode(content));
        return true;
      } catch {
        return false;
      }
    } catch {
      return false;
    }
  }

  /**
   * Get pending conflicts
   */
  getPendingConflicts(): Conflict[] {
    return [...this.pendingConflicts];
  }

  /**
   * Generate unique conflict ID
   */
  private generateConflictId(): string {
    return `conflict-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }
}
