import 'dart:async';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';

import '../models/office.dart';
import '../models/ticket.dart';
import '../models/ticket_message.dart';
import 'profile_provider.dart';
import 'supabase_provider.dart';
import 'user_offices_provider.dart';
import 'tasks_provider.dart';
import 'stream_recovery.dart';
import 'realtime_controller.dart';

final officesProvider = StreamProvider<List<Office>>((ref) {
  final client = ref.watch(supabaseClientProvider);

  final wrapper = StreamRecoveryWrapper<Office>(
    stream: client.from('offices').stream(primaryKey: ['id']).order('name'),
    onPollData: () async {
      final data = await client.from('offices').select().order('name');
      return data.map(Office.fromMap).toList();
    },
    fromMap: Office.fromMap,
    channelName: 'offices',
    onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
  );

  ref.onDispose(wrapper.dispose);
  return wrapper.stream.map((result) => result.data);
});

final officesOnceProvider = FutureProvider<List<Office>>((ref) async {
  final client = ref.watch(supabaseClientProvider);
  final rows = await client.from('offices').select().order('name');
  return (rows as List<dynamic>)
      .map((row) => Office.fromMap(row as Map<String, dynamic>))
      .toList();
});

/// Office query parameters for server-side pagination.
class OfficeQuery {
  /// Creates office query parameters.
  const OfficeQuery({this.offset = 0, this.limit = 50, this.searchQuery = ''});

  /// Offset for pagination.
  final int offset;

  /// Number of items per page (default: 50).
  final int limit;

  /// Full text search query.
  final String searchQuery;

  OfficeQuery copyWith({int? offset, int? limit, String? searchQuery}) {
    return OfficeQuery(
      offset: offset ?? this.offset,
      limit: limit ?? this.limit,
      searchQuery: searchQuery ?? this.searchQuery,
    );
  }
}

final officesQueryProvider = StateProvider<OfficeQuery>(
  (ref) => const OfficeQuery(),
);

final officesControllerProvider = Provider<OfficesController>((ref) {
  final client = ref.watch(supabaseClientProvider);
  return OfficesController(client);
});

/// Ticket query parameters for server-side pagination and filtering.
class TicketQuery {
  /// Creates ticket query parameters.
  const TicketQuery({
    this.offset = 0,
    this.limit = 50,
    this.searchQuery = '',
    this.officeId,
    this.status,
    this.dateRange,
  });

  /// Offset for pagination.
  final int offset;

  /// Number of items per page (default: 50).
  final int limit;

  /// Full text search query.
  final String searchQuery;

  /// Filter by office ID.
  final String? officeId;

  /// Filter by status.
  final String? status;

  /// Filter by date range.
  /// Filter by date range.
  final DateTimeRange? dateRange;

  TicketQuery copyWith({
    int? offset,
    int? limit,
    String? searchQuery,
    String? officeId,
    String? status,
    DateTimeRange? dateRange,
  }) {
    return TicketQuery(
      offset: offset ?? this.offset,
      limit: limit ?? this.limit,
      searchQuery: searchQuery ?? this.searchQuery,
      officeId: officeId ?? this.officeId,
      status: status ?? this.status,
      dateRange: dateRange ?? this.dateRange,
    );
  }
}

/// Builds the isolate payload from a list of [Ticket] objects and the current
/// query/access context. Extracted so the initial REST seed and the realtime
/// stream listener can share the same logic without duplication.
Map<String, dynamic> _buildTicketPayload({
  required List<Ticket> tickets,
  required bool isGlobal,
  required List<String> allowedOfficeIds,
  required TicketQuery query,
}) {
  final rowsList = tickets
      .map(
        (ticket) => <String, dynamic>{
          'id': ticket.id,
          'subject': ticket.subject,
          'description': ticket.description,
          'status': ticket.status,
          'office_id': ticket.officeId,
          'creator_id': ticket.creatorId,
          'created_at': ticket.createdAt.toIso8601String(),
          'responded_at': ticket.respondedAt?.toIso8601String(),
          'promoted_at': ticket.promotedAt?.toIso8601String(),
          'closed_at': ticket.closedAt?.toIso8601String(),
        },
      )
      .toList();

  return {
    'rows': rowsList,
    'isGlobal': isGlobal,
    'allowedOfficeIds': allowedOfficeIds,
    'offset': query.offset,
    'limit': query.limit,
    'searchQuery': query.searchQuery,
    'officeId': query.officeId,
    'status': query.status,
    'dateStart': query.dateRange?.start.millisecondsSinceEpoch,
    'dateEnd': query.dateRange?.end.millisecondsSinceEpoch,
  };
}

final ticketsProvider = StreamProvider<List<Ticket>>((ref) {
  final client = ref.watch(supabaseClientProvider);
  final profileAsync = ref.watch(currentProfileProvider);
  final assignmentsAsync = ref.watch(userOfficesProvider);
  final query = ref.watch(ticketsQueryProvider);

  final profile = profileAsync.valueOrNull;
  if (profile == null) {
    return Stream.value(const <Ticket>[]);
  }

  final isGlobal =
      profile.role == 'admin' ||
      profile.role == 'dispatcher' ||
      profile.role == 'it_staff';

  final allowedOfficeIds =
      assignmentsAsync.valueOrNull
          ?.where((a) => a.userId == profile.id)
          .map((a) => a.officeId)
          .toList() ??
      <String>[];

  // Wrap realtime stream with recovery logic
  final wrapper = StreamRecoveryWrapper<Ticket>(
    stream: client.from('tickets').stream(primaryKey: ['id']),
    onPollData: () async {
      final data = await client.from('tickets').select();
      return data.cast<Map<String, dynamic>>().map(Ticket.fromMap).toList();
    },
    fromMap: Ticket.fromMap,
    channelName: 'tickets',
    onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
  );

  ref.onDispose(wrapper.dispose);

  var lastResultHash = '';
  Timer? debounceTimer;
  // broadcast() so Riverpod and any other listener can both receive events.
  final controller = StreamController<List<Ticket>>.broadcast();

  void emitDebounced(List<Ticket> tickets) {
    debounceTimer?.cancel();
    debounceTimer = Timer(const Duration(milliseconds: 150), () {
      if (!controller.isClosed) controller.add(tickets);
    });
  }

  ref.onDispose(() {
    debounceTimer?.cancel();
    controller.close();
  });

  // ── Immediate REST seed ───────────────────────────────────────────────────
  // Fire a one-shot HTTP fetch right now so the UI can render before the
  // WebSocket realtime channel is fully established.  This eliminates the
  // loading delay on web (WebSocket ~200-500 ms) and the initial flash on
  // mobile.  The realtime stream takes over afterwards; the hash check below
  // prevents a duplicate rebuild if both arrive with identical data.
  unawaited(
    Future(() async {
      try {
        final data = await client.from('tickets').select();
        final raw = data
            .cast<Map<String, dynamic>>()
            .map(Ticket.fromMap)
            .toList();
        final payload = _buildTicketPayload(
          tickets: raw,
          isGlobal: isGlobal,
          allowedOfficeIds: allowedOfficeIds,
          query: query,
        );
        final processed = await compute(_processTicketsInIsolate, payload);
        final tickets = (processed as List<dynamic>)
            .cast<Map<String, dynamic>>()
            .map(Ticket.fromMap)
            .toList();
        final hash = tickets.fold('', (h, t) => '$h${t.id}');
        if (!controller.isClosed && hash != lastResultHash) {
          lastResultHash = hash;
          controller.add(tickets); // emit immediately – no debounce
        }
      } catch (e) {
        debugPrint('[ticketsProvider] initial seed error: $e');
      }
    }),
  );

  // ── Realtime stream ───────────────────────────────────────────────────────
  // Processes every realtime event through the same isolate. Debounced so
  // rapid consecutive events (e.g. bulk inserts) don't cause repeated renders.
  final wrapperSub = wrapper.stream
      .asyncMap((result) async {
        final payload = _buildTicketPayload(
          tickets: result.data,
          isGlobal: isGlobal,
          allowedOfficeIds: allowedOfficeIds,
          query: query,
        );
        final processed = await compute(_processTicketsInIsolate, payload);
        return (processed as List<dynamic>)
            .cast<Map<String, dynamic>>()
            .map(Ticket.fromMap)
            .toList();
      })
      .listen(
        (tickets) {
          final hash = tickets.fold('', (h, t) => '$h${t.id}');
          if (hash != lastResultHash) {
            lastResultHash = hash;
            emitDebounced(tickets);
          }
        },
        onError: (Object e) {
          debugPrint('[ticketsProvider] stream error: $e');
          // Don't forward errors — the wrapper handles recovery internally.
        },
      );

  ref.onDispose(wrapperSub.cancel);

  return controller.stream;
});

// Runs inside a background isolate. Accepts a serializable payload and
// returns a list of ticket maps after filtering/sorting/pagination.
List<Map<String, dynamic>> _processTicketsInIsolate(
  Map<String, dynamic> payload,
) {
  final rows = (payload['rows'] as List).cast<Map<String, dynamic>>();
  var list = List<Map<String, dynamic>>.from(rows);

  final isGlobal = payload['isGlobal'] as bool? ?? false;
  final allowedOfficeIds =
      (payload['allowedOfficeIds'] as List?)?.cast<String>().toSet() ??
      <String>{};

  if (!isGlobal) {
    if (allowedOfficeIds.isEmpty) {
      return <Map<String, dynamic>>[];
    }
    list = list
        .where((t) => allowedOfficeIds.contains(t['office_id']))
        .toList();
  }

  final officeId = payload['officeId'] as String?;
  if (officeId != null) {
    list = list.where((t) => t['office_id'] == officeId).toList();
  }
  final status = payload['status'] as String?;
  if (status != null) {
    list = list.where((t) => t['status'] == status).toList();
  }

  final searchQuery = (payload['searchQuery'] as String?) ?? '';
  if (searchQuery.isNotEmpty) {
    final q = searchQuery.toLowerCase();
    list = list.where((t) {
      final subj = (t['subject'] as String?)?.toLowerCase() ?? '';
      final desc = (t['description'] as String?)?.toLowerCase() ?? '';
      return subj.contains(q) || desc.contains(q);
    }).toList();
  }

  // Parse created_at timestamp from map. `created_at` may be ISO strings or timestamps.
  int parseCreatedAt(Map<String, dynamic> m) {
    final v = m['created_at'];
    if (v == null) return 0;
    if (v is int) return v;
    if (v is double) return v.toInt();
    if (v is String) {
      try {
        return DateTime.parse(v).millisecondsSinceEpoch;
      } catch (_) {
        return 0;
      }
    }
    return 0;
  }

  int statusRank(String s) {
    switch (s) {
      case 'pending':
        return 0;
      case 'promoted':
        return 1;
      case 'closed':
        return 2;
      default:
        return 3;
    }
  }

  // Sort by status rank first (pending → promoted → closed),
  // then by created_at descending (newest first)
  list.sort((a, b) {
    final ra = statusRank((a['status'] as String?) ?? '');
    final rb = statusRank((b['status'] as String?) ?? '');
    final rcmp = ra.compareTo(rb);
    if (rcmp != 0) return rcmp;

    // Same status: sort by created_at descending (newest first)
    return parseCreatedAt(b).compareTo(parseCreatedAt(a));
  });

  final start = (payload['offset'] as int?) ?? 0;
  final limit = (payload['limit'] as int?) ?? 50;
  final end = (start + limit).clamp(0, list.length);
  if (start >= list.length) return <Map<String, dynamic>>[];
  return list.sublist(start, end);
}

/// Provider for ticket query parameters.
final ticketsQueryProvider = StateProvider<TicketQuery>(
  (ref) => const TicketQuery(),
);

/// Derived provider that selects a single [Ticket] by ID from the tickets list.
///
/// Because [Ticket] implements `==`, this provider only notifies watchers when
/// the specific ticket's data actually changes — not when unrelated tickets in
/// the list are updated.  Use this in detail screens to avoid full-list rebuilds.
final ticketByIdProvider = Provider.family<Ticket?, String>((ref, ticketId) {
  return ref
      .watch(ticketsProvider)
      .valueOrNull
      ?.where((t) => t.id == ticketId)
      .firstOrNull;
});

final ticketMessagesProvider =
    StreamProvider.family<List<TicketMessage>, String>((ref, ticketId) {
      final client = ref.watch(supabaseClientProvider);

      final wrapper = StreamRecoveryWrapper<TicketMessage>(
        stream: client
            .from('ticket_messages')
            .stream(primaryKey: ['id'])
            .eq('ticket_id', ticketId)
            .order('created_at', ascending: false),
        onPollData: () async {
          final data = await client
              .from('ticket_messages')
              .select()
              .eq('ticket_id', ticketId)
              .order('created_at', ascending: false);
          return data.map(TicketMessage.fromMap).toList();
        },
        fromMap: TicketMessage.fromMap,
        channelName: 'ticket_messages:$ticketId',
        onStatusChanged: ref
            .read(realtimeControllerProvider)
            .handleChannelStatus,
      );

      ref.onDispose(wrapper.dispose);
      return wrapper.stream.map((result) => result.data);
    });

final ticketMessagesAllProvider = StreamProvider<List<TicketMessage>>((ref) {
  final client = ref.watch(supabaseClientProvider);

  final wrapper = StreamRecoveryWrapper<TicketMessage>(
    stream: client
        .from('ticket_messages')
        .stream(primaryKey: ['id'])
        .order('created_at', ascending: false),
    onPollData: () async {
      final data = await client
          .from('ticket_messages')
          .select()
          .order('created_at', ascending: false);
      return data.map(TicketMessage.fromMap).toList();
    },
    fromMap: TicketMessage.fromMap,
    channelName: 'ticket_messages_all',
    onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
  );

  ref.onDispose(wrapper.dispose);
  return wrapper.stream.map((result) => result.data);
});

final taskMessagesProvider = StreamProvider.family<List<TicketMessage>, String>(
  (ref, taskId) {
    final client = ref.watch(supabaseClientProvider);

    final wrapper = StreamRecoveryWrapper<TicketMessage>(
      stream: client
          .from('ticket_messages')
          .stream(primaryKey: ['id'])
          .eq('task_id', taskId)
          .order('created_at', ascending: false),
      onPollData: () async {
        final data = await client
            .from('ticket_messages')
            .select()
            .eq('task_id', taskId)
            .order('created_at', ascending: false);
        return data.map(TicketMessage.fromMap).toList();
      },
      fromMap: TicketMessage.fromMap,
      channelName: 'task_messages:$taskId',
      onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
    );

    ref.onDispose(wrapper.dispose);
    return wrapper.stream.map((result) => result.data);
  },
);

final ticketsControllerProvider = Provider<TicketsController>((ref) {
  final client = ref.watch(supabaseClientProvider);
  return TicketsController(client);
});

class TicketsController {
  TicketsController(this._client);

  final SupabaseClient _client;

  Future<void> createTicket({
    required String subject,
    required String description,
    required String officeId,
  }) async {
    final actorId = _client.auth.currentUser?.id;
    final data = await _client
        .from('tickets')
        .insert({
          'subject': subject,
          'description': description,
          'office_id': officeId,
          'creator_id': _client.auth.currentUser?.id,
        })
        .select('id')
        .single();
    final ticketId = data['id'] as String?;
    if (ticketId == null) return;
    unawaited(_notifyCreated(ticketId: ticketId, actorId: actorId));
  }

  Future<void> _notifyCreated({
    required String ticketId,
    required String? actorId,
  }) async {
    try {
      final recipients = await _fetchRoleUserIds(
        roles: const ['dispatcher', 'it_staff'],
        excludeUserId: actorId,
      );
      if (recipients.isEmpty) return;
      final rows = recipients
          .map(
            (userId) => {
              'user_id': userId,
              'actor_id': actorId,
              'ticket_id': ticketId,
              'type': 'created',
            },
          )
          .toList();
      await _client.from('notifications').insert(rows);
      // Send FCM pushes for ticket creation
      try {
        String actorName = 'Someone';
        if (actorId != null && actorId.isNotEmpty) {
          try {
            final p = await _client
                .from('profiles')
                .select('full_name,display_name,name')
                .eq('id', actorId)
                .maybeSingle();
            if (p != null) {
              if (p['full_name'] != null) {
                actorName = p['full_name'].toString();
              } else if (p['display_name'] != null) {
                actorName = p['display_name'].toString();
              } else if (p['name'] != null) {
                actorName = p['name'].toString();
              }
            }
          } catch (_) {}
        }

        String? ticketNumber;
        try {
          final t = await _client
              .from('tickets')
              .select('ticket_number')
              .eq('id', ticketId)
              .maybeSingle();
          if (t != null && t['ticket_number'] != null) {
            ticketNumber = t['ticket_number'].toString();
          }
        } catch (_) {}

        final title = '$actorName created a new ticket';
        final body = ticketNumber != null
            ? '$actorName created ticket #$ticketNumber'
            : '$actorName created a new ticket';

        await _client.functions.invoke(
          'send_fcm',
          body: {
            'user_ids': recipients,
            'title': title,
            'body': body,
            'data': {
              'ticket_id': ticketId,
              'ticket_number': ?ticketNumber,
              'type': 'created',
            },
          },
        );
      } catch (e) {
        // non-fatal
        debugPrint('ticket notifyCreated push error: $e');
      }
    } catch (_) {
      return;
    }
  }

  Future<List<String>> _fetchRoleUserIds({
    required List<String> roles,
    required String? excludeUserId,
  }) async {
    try {
      final data = await _client
          .from('profiles')
          .select('id, role')
          .inFilter('role', roles);
      final rows = data as List<dynamic>;
      final ids = rows
          .map((row) => row['id'] as String?)
          .whereType<String>()
          .where((id) => id.isNotEmpty && id != excludeUserId)
          .toList();
      return ids;
    } catch (_) {
      return [];
    }
  }

  Future<TicketMessage> sendTicketMessage({
    required String ticketId,
    required String content,
  }) async {
    final data = await _client
        .from('ticket_messages')
        .insert({
          'ticket_id': ticketId,
          'content': content,
          'sender_id': _client.auth.currentUser?.id,
        })
        .select()
        .single();
    return TicketMessage.fromMap(data);
  }

  Future<TicketMessage> sendTaskMessage({
    required String taskId,
    required String? ticketId,
    required String content,
  }) async {
    final payload = <String, dynamic>{
      'task_id': taskId,
      'content': content,
      'sender_id': _client.auth.currentUser?.id,
    };
    if (ticketId != null) {
      payload['ticket_id'] = ticketId;
    }
    final data = await _client
        .from('ticket_messages')
        .insert(payload)
        .select()
        .single();
    return TicketMessage.fromMap(data);
  }

  Future<void> updateTicketStatus({
    required String ticketId,
    required String status,
  }) async {
    await _client.from('tickets').update({'status': status}).eq('id', ticketId);

    // If ticket is promoted, create a linked Task (only once) — the
    // TasksController.createTask already runs auto-assignment on creation.
    if (status == 'promoted') {
      try {
        final existing = await _client
            .from('tasks')
            .select('id')
            .eq('ticket_id', ticketId)
            .maybeSingle();
        if (existing != null) return;

        final ticketRow = await _client
            .from('tickets')
            .select('subject, description, office_id')
            .eq('id', ticketId)
            .maybeSingle();

        final title = (ticketRow?['subject'] as String?) ?? 'Task from ticket';
        final description = (ticketRow?['description'] as String?) ?? '';
        final officeId = ticketRow?['office_id'] as String?;

        final tasksCtrl = TasksController(_client);
        await tasksCtrl.createTask(
          title: title,
          description: description,
          officeId: officeId,
          ticketId: ticketId,
        );
      } catch (_) {
        // best-effort — don't fail the ticket status update
      }
    }
  }

  /// Update editable ticket fields such as subject, description, and office.
  Future<void> updateTicket({
    required String ticketId,
    String? subject,
    String? description,
    String? officeId,
  }) async {
    final payload = <String, dynamic>{};
    if (subject != null) payload['subject'] = subject;
    if (description != null) payload['description'] = description;
    if (officeId != null) payload['office_id'] = officeId;
    if (payload.isEmpty) return;

    await _client.from('tickets').update(payload).eq('id', ticketId);

    // record an activity row for edit operations (best-effort)
    try {
      final actorId = _client.auth.currentUser?.id;
      await _client.from('ticket_messages').insert({
        'ticket_id': ticketId,
        'sender_id': actorId,
        'content': 'Ticket updated',
      });
    } catch (_) {}
  }
}

class OfficesController {
  OfficesController(this._client);

  final SupabaseClient _client;

  Future<void> createOffice({required String name, String? serviceId}) async {
    final payload = {'name': name};
    if (serviceId != null) payload['service_id'] = serviceId;
    await _client.from('offices').insert(payload);
  }

  Future<void> updateOffice({
    required String id,
    required String name,
    String? serviceId,
  }) async {
    final payload = {'name': name};
    if (serviceId != null) payload['service_id'] = serviceId;
    await _client.from('offices').update(payload).eq('id', id);
  }

  Future<void> deleteOffice({required String id}) async {
    await _client.from('offices').delete().eq('id', id);
  }
}
