import { Repository, IsNull, In } from 'typeorm';
import { AppDataSource } from '../../../config/typeorm.js';
import {
  MobileSession,
  MobileSessionStatus,
  OfflineSyncQueue,
  SyncStatus,
  SyncOperation,
  PushToken,
  PushProvider,
} from '../entities/index.js';
import { NotFoundError, ValidationError, ConflictError } from '../../../shared/types/index.js';
import { logger } from '../../../shared/utils/logger.js';

// ===== DTOs =====

export interface CreateSessionDto {
  userId: string;
  deviceId: string;
  branchId?: string;
  activeProfileId?: string;
  activeProfileCode?: string;
  appVersion?: string;
  platform?: string;
  osVersion?: string;
  expiresAt?: Date;
}

export interface SessionFilters {
  userId?: string;
  deviceId?: string;
  status?: MobileSessionStatus;
  page?: number;
  limit?: number;
}

export interface RegisterPushTokenDto {
  userId: string;
  deviceId: string;
  token: string;
  platform: string;
  provider?: PushProvider;
  subscribedTopics?: string[];
}

export interface UpdatePushTokenDto {
  token?: string;
  isActive?: boolean;
  subscribedTopics?: string[];
}

export interface AddToQueueDto {
  userId: string;
  deviceId: string;
  sessionId?: string;
  entityType: string;
  entityId?: string;
  operation: SyncOperation;
  payload: Record<string, any>;
  metadata?: Record<string, any>;
  sequenceNumber: number;
  dependsOn?: string;
  maxRetries?: number;
}

export interface QueueFilters {
  userId?: string;
  deviceId?: string;
  status?: SyncStatus;
  entityType?: string;
  page?: number;
  limit?: number;
}

// ===== MobileService Class =====

class MobileService {
  private sessionRepository: Repository<MobileSession>;
  private pushTokenRepository: Repository<PushToken>;
  private syncQueueRepository: Repository<OfflineSyncQueue>;

  constructor() {
    this.sessionRepository = AppDataSource.getRepository(MobileSession);
    this.pushTokenRepository = AppDataSource.getRepository(PushToken);
    this.syncQueueRepository = AppDataSource.getRepository(OfflineSyncQueue);
  }

  // ===== Session Management =====

  /**
   * Create a new mobile session
   */
  async createSession(
    tenantId: string,
    dto: CreateSessionDto
  ): Promise<MobileSession> {
    try {
      const sessionData: Partial<MobileSession> = {
        tenantId,
        userId: dto.userId,
        deviceId: dto.deviceId,
        status: 'active',
        isOfflineMode: false,
        pendingSyncCount: 0,
        startedAt: new Date(),
        lastActivityAt: new Date(),
      };

      if (dto.branchId) sessionData.branchId = dto.branchId;
      if (dto.activeProfileId) sessionData.activeProfileId = dto.activeProfileId;
      if (dto.activeProfileCode) sessionData.activeProfileCode = dto.activeProfileCode;
      if (dto.appVersion) sessionData.appVersion = dto.appVersion;
      if (dto.platform) sessionData.platform = dto.platform;
      if (dto.osVersion) sessionData.osVersion = dto.osVersion;
      if (dto.expiresAt) sessionData.expiresAt = dto.expiresAt;

      const session = this.sessionRepository.create(sessionData);
      await this.sessionRepository.save(session);

      logger.info('Mobile session created', {
        sessionId: session.id,
        tenantId,
        userId: dto.userId,
        deviceId: dto.deviceId,
      });

      return session;
    } catch (error) {
      logger.error('Error creating mobile session', {
        error: (error as Error).message,
        tenantId,
        userId: dto.userId,
      });
      throw error;
    }
  }

  /**
   * Get active sessions for a tenant with optional filters
   */
  async getActiveSessions(
    tenantId: string,
    filters: SessionFilters = {}
  ): Promise<{ data: MobileSession[]; total: number }> {
    try {
      const { userId, deviceId, status, page = 1, limit = 20 } = filters;
      const skip = (page - 1) * limit;

      const queryBuilder = this.sessionRepository
        .createQueryBuilder('session')
        .where('session.tenantId = :tenantId', { tenantId });

      if (userId) {
        queryBuilder.andWhere('session.userId = :userId', { userId });
      }

      if (deviceId) {
        queryBuilder.andWhere('session.deviceId = :deviceId', { deviceId });
      }

      if (status) {
        queryBuilder.andWhere('session.status = :status', { status });
      } else {
        queryBuilder.andWhere('session.status = :status', { status: 'active' });
      }

      const total = await queryBuilder.getCount();

      const data = await queryBuilder
        .orderBy('session.lastActivityAt', 'DESC')
        .skip(skip)
        .take(limit)
        .getMany();

      logger.debug('Active sessions retrieved', { tenantId, count: data.length, total });

      return { data, total };
    } catch (error) {
      logger.error('Error retrieving active sessions', {
        error: (error as Error).message,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Revoke (terminate) a mobile session
   */
  async revokeSession(
    tenantId: string,
    sessionId: string
  ): Promise<MobileSession> {
    try {
      const session = await this.sessionRepository.findOne({
        where: { id: sessionId, tenantId },
      });

      if (!session) {
        throw new NotFoundError('Sesion movil no encontrada');
      }

      if (session.status === 'terminated') {
        throw new ValidationError('La sesion ya fue terminada');
      }

      session.status = 'terminated';
      session.endedAt = new Date();

      await this.sessionRepository.save(session);

      logger.info('Mobile session revoked', {
        sessionId,
        tenantId,
        userId: session.userId,
      });

      return session;
    } catch (error) {
      logger.error('Error revoking mobile session', {
        error: (error as Error).message,
        tenantId,
        sessionId,
      });
      throw error;
    }
  }

  /**
   * Find sessions by device ID
   */
  async findByDeviceId(
    tenantId: string,
    deviceId: string
  ): Promise<MobileSession[]> {
    try {
      const sessions = await this.sessionRepository.find({
        where: { tenantId, deviceId },
        order: { lastActivityAt: 'DESC' },
      });

      logger.debug('Sessions found by device', { tenantId, deviceId, count: sessions.length });

      return sessions;
    } catch (error) {
      logger.error('Error finding sessions by device', {
        error: (error as Error).message,
        tenantId,
        deviceId,
      });
      throw error;
    }
  }

  /**
   * Find sessions by user ID
   */
  async findByUserId(
    tenantId: string,
    userId: string
  ): Promise<MobileSession[]> {
    try {
      const sessions = await this.sessionRepository.find({
        where: { tenantId, userId },
        order: { lastActivityAt: 'DESC' },
      });

      logger.debug('Sessions found by user', { tenantId, userId, count: sessions.length });

      return sessions;
    } catch (error) {
      logger.error('Error finding sessions by user', {
        error: (error as Error).message,
        tenantId,
        userId,
      });
      throw error;
    }
  }

  // ===== Push Token Management =====

  /**
   * Register a new push token
   */
  async registerPushToken(
    tenantId: string,
    dto: RegisterPushTokenDto
  ): Promise<PushToken> {
    try {
      // Check if a token already exists for this device+platform combination
      const existing = await this.pushTokenRepository.findOne({
        where: {
          tenantId,
          deviceId: dto.deviceId,
          platform: dto.platform,
        },
      });

      if (existing) {
        // Update the existing token instead of creating a duplicate
        existing.token = dto.token;
        existing.userId = dto.userId;
        existing.isActive = true;
        existing.isValid = true;
        existing.invalidReason = null as any;
        existing.lastUsedAt = new Date();

        if (dto.provider) existing.provider = dto.provider;
        if (dto.subscribedTopics) existing.subscribedTopics = dto.subscribedTopics;

        await this.pushTokenRepository.save(existing);

        logger.info('Push token updated (existing device)', {
          tokenId: existing.id,
          tenantId,
          deviceId: dto.deviceId,
        });

        return existing;
      }

      const tokenData: Partial<PushToken> = {
        tenantId,
        userId: dto.userId,
        deviceId: dto.deviceId,
        token: dto.token,
        platform: dto.platform,
        provider: dto.provider || 'firebase',
        isActive: true,
        isValid: true,
        subscribedTopics: dto.subscribedTopics || [],
        lastUsedAt: new Date(),
      };

      const pushToken = this.pushTokenRepository.create(tokenData);
      await this.pushTokenRepository.save(pushToken);

      logger.info('Push token registered', {
        tokenId: pushToken.id,
        tenantId,
        userId: dto.userId,
        deviceId: dto.deviceId,
        platform: dto.platform,
      });

      return pushToken;
    } catch (error) {
      logger.error('Error registering push token', {
        error: (error as Error).message,
        tenantId,
        deviceId: dto.deviceId,
      });
      throw error;
    }
  }

  /**
   * Update an existing push token
   */
  async updatePushToken(
    tenantId: string,
    tokenId: string,
    dto: UpdatePushTokenDto
  ): Promise<PushToken> {
    try {
      const pushToken = await this.pushTokenRepository.findOne({
        where: { id: tokenId, tenantId },
      });

      if (!pushToken) {
        throw new NotFoundError('Token de push no encontrado');
      }

      if (dto.token !== undefined) pushToken.token = dto.token;
      if (dto.isActive !== undefined) pushToken.isActive = dto.isActive;
      if (dto.subscribedTopics !== undefined) pushToken.subscribedTopics = dto.subscribedTopics;

      pushToken.lastUsedAt = new Date();

      await this.pushTokenRepository.save(pushToken);

      logger.info('Push token updated', {
        tokenId,
        tenantId,
      });

      return pushToken;
    } catch (error) {
      logger.error('Error updating push token', {
        error: (error as Error).message,
        tenantId,
        tokenId,
      });
      throw error;
    }
  }

  /**
   * Remove (deactivate) a push token
   */
  async removePushToken(
    tenantId: string,
    tokenId: string
  ): Promise<void> {
    try {
      const pushToken = await this.pushTokenRepository.findOne({
        where: { id: tokenId, tenantId },
      });

      if (!pushToken) {
        throw new NotFoundError('Token de push no encontrado');
      }

      pushToken.isActive = false;
      pushToken.isValid = false;
      pushToken.invalidReason = 'Removed by user';

      await this.pushTokenRepository.save(pushToken);

      logger.info('Push token removed', {
        tokenId,
        tenantId,
        userId: pushToken.userId,
      });
    } catch (error) {
      logger.error('Error removing push token', {
        error: (error as Error).message,
        tenantId,
        tokenId,
      });
      throw error;
    }
  }

  // ===== Offline Sync Queue =====

  /**
   * Get sync queue items with optional filters
   */
  async getQueueItems(
    tenantId: string,
    filters: QueueFilters = {}
  ): Promise<{ data: OfflineSyncQueue[]; total: number }> {
    try {
      const { userId, deviceId, status, entityType, page = 1, limit = 20 } = filters;
      const skip = (page - 1) * limit;

      const queryBuilder = this.syncQueueRepository
        .createQueryBuilder('queue')
        .where('queue.tenantId = :tenantId', { tenantId });

      if (userId) {
        queryBuilder.andWhere('queue.userId = :userId', { userId });
      }

      if (deviceId) {
        queryBuilder.andWhere('queue.deviceId = :deviceId', { deviceId });
      }

      if (status) {
        queryBuilder.andWhere('queue.status = :status', { status });
      }

      if (entityType) {
        queryBuilder.andWhere('queue.entityType = :entityType', { entityType });
      }

      const total = await queryBuilder.getCount();

      const data = await queryBuilder
        .orderBy('queue.sequenceNumber', 'ASC')
        .skip(skip)
        .take(limit)
        .getMany();

      logger.debug('Sync queue items retrieved', { tenantId, count: data.length, total });

      return { data, total };
    } catch (error) {
      logger.error('Error retrieving sync queue items', {
        error: (error as Error).message,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Add an item to the offline sync queue
   */
  async addToQueue(
    tenantId: string,
    dto: AddToQueueDto
  ): Promise<OfflineSyncQueue> {
    try {
      const queueData: Partial<OfflineSyncQueue> = {
        tenantId,
        userId: dto.userId,
        deviceId: dto.deviceId,
        entityType: dto.entityType,
        operation: dto.operation,
        payload: dto.payload,
        metadata: dto.metadata || {},
        sequenceNumber: dto.sequenceNumber,
        status: 'pending',
        retryCount: 0,
        maxRetries: dto.maxRetries ?? 3,
      };

      if (dto.sessionId) queueData.sessionId = dto.sessionId;
      if (dto.entityId) queueData.entityId = dto.entityId;
      if (dto.dependsOn) queueData.dependsOn = dto.dependsOn;

      const queueItem = this.syncQueueRepository.create(queueData);
      await this.syncQueueRepository.save(queueItem);

      logger.info('Item added to sync queue', {
        queueItemId: queueItem.id,
        tenantId,
        entityType: dto.entityType,
        operation: dto.operation,
        sequenceNumber: dto.sequenceNumber,
      });

      return queueItem;
    } catch (error) {
      logger.error('Error adding item to sync queue', {
        error: (error as Error).message,
        tenantId,
        entityType: dto.entityType,
      });
      throw error;
    }
  }

  /**
   * Process a sync queue item (mark as processing, then completed or failed)
   */
  async processQueueItem(
    tenantId: string,
    queueItemId: string
  ): Promise<OfflineSyncQueue> {
    try {
      const queueItem = await this.syncQueueRepository.findOne({
        where: { id: queueItemId, tenantId },
      });

      if (!queueItem) {
        throw new NotFoundError('Elemento de cola de sincronizacion no encontrado');
      }

      if (queueItem.status === 'completed') {
        throw new ValidationError('El elemento ya fue procesado');
      }

      if (queueItem.status === 'processing') {
        throw new ValidationError('El elemento ya esta en procesamiento');
      }

      // Check dependency
      if (queueItem.dependsOn) {
        const dependency = await this.syncQueueRepository.findOne({
          where: { id: queueItem.dependsOn, tenantId },
        });

        if (dependency && dependency.status !== 'completed') {
          throw new ValidationError(
            `El elemento depende de ${queueItem.dependsOn} que aun no ha sido completado`
          );
        }
      }

      queueItem.status = 'processing';
      await this.syncQueueRepository.save(queueItem);

      // Mark as completed
      queueItem.status = 'completed';
      queueItem.processedAt = new Date();

      await this.syncQueueRepository.save(queueItem);

      logger.info('Sync queue item processed', {
        queueItemId,
        tenantId,
        entityType: queueItem.entityType,
        operation: queueItem.operation,
      });

      return queueItem;
    } catch (error) {
      // If the item was set to processing but failed, mark it as failed
      if (error instanceof NotFoundError || error instanceof ValidationError) {
        throw error;
      }

      try {
        const failedItem = await this.syncQueueRepository.findOne({
          where: { id: queueItemId, tenantId },
        });
        if (failedItem && failedItem.status === 'processing') {
          failedItem.status = 'failed';
          failedItem.retryCount += 1;
          failedItem.lastError = (error as Error).message;
          await this.syncQueueRepository.save(failedItem);
        }
      } catch (saveError) {
        logger.error('Error saving failed queue item status', {
          error: (saveError as Error).message,
          queueItemId,
        });
      }

      logger.error('Error processing sync queue item', {
        error: (error as Error).message,
        tenantId,
        queueItemId,
      });
      throw error;
    }
  }

  /**
   * Clear completed/failed queue items for a device
   */
  async clearQueue(
    tenantId: string,
    deviceId: string,
    statuses: SyncStatus[] = ['completed', 'failed']
  ): Promise<number> {
    try {
      const result = await this.syncQueueRepository.delete({
        tenantId,
        deviceId,
        status: In(statuses),
      });

      const deletedCount = result.affected || 0;

      logger.info('Sync queue cleared', {
        tenantId,
        deviceId,
        deletedCount,
        statuses,
      });

      return deletedCount;
    } catch (error) {
      logger.error('Error clearing sync queue', {
        error: (error as Error).message,
        tenantId,
        deviceId,
      });
      throw error;
    }
  }
}

// ===== Export Singleton Instance =====

export const mobileService = new MobileService();
