import 'dart:convert';

import 'package:dio/dio.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:kasy_kit/core/config/app_env.dart';
import 'package:kasy_kit/core/states/translations.dart';
import 'package:kasy_kit/core/states/user_state_notifier.dart';
import 'package:kasy_kit/features/llm_chat/api/llm_chat_api.dart';
import 'package:kasy_kit/features/llm_chat/api/llm_chat_message_entity.dart';
import 'package:logger/logger.dart';

/// Maximum number of recent messages sent to the AI as context.
///
/// Lower values = fewer tokens = cheaper + faster responses.
/// Higher values = more conversational memory but higher cost.
///
/// Each exchange = 2 messages (1 user + 1 assistant).
/// Default: 20 messages = 10 exchanges of back-and-forth.
///
/// To change: update this constant and redeploy (no backend change needed).
const int _kMaxContextMessages = 20;

/// A single message in the chat conversation.
class ChatMessage {
  final String role; // 'user' or 'assistant'
  final String content;

  const ChatMessage({required this.role, required this.content});

  factory ChatMessage.user(String content) =>
      ChatMessage(role: 'user', content: content);

  factory ChatMessage.assistant(String content) =>
      ChatMessage(role: 'assistant', content: content);

  factory ChatMessage.fromEntity(LlmChatMessageEntity entity) =>
      ChatMessage(role: entity.role, content: entity.content);

  LlmChatMessageEntity toEntity() => LlmChatMessageEntity(
    role: role,
    content: content,
    createdAt: DateTime.now(),
  );
}

/// UI state for the LLM chat screen.
class LlmChatState {
  final List<ChatMessage> messages;

  /// True while the HTTP request to the LLM backend is in-flight.
  final bool isReplying;

  /// True once the first SSE chunk has been received.
  /// While true the last message in [messages] is the partial assistant reply.
  final bool streamingStarted;

  const LlmChatState({
    required this.messages,
    this.isReplying = false,
    this.streamingStarted = false,
  });

  LlmChatState copyWith({
    List<ChatMessage>? messages,
    bool? isReplying,
    bool? streamingStarted,
  }) {
    return LlmChatState(
      messages: messages ?? this.messages,
      isReplying: isReplying ?? this.isReplying,
      streamingStarted: streamingStarted ?? this.streamingStarted,
    );
  }
}

/// Manages LLM chat state: loads history from the backend on init,
/// persists each message, and streams the assistant reply word-by-word via SSE.
final llmChatNotifierProvider =
    AsyncNotifierProvider<LlmChatNotifier, LlmChatState>(LlmChatNotifier.new);

class LlmChatNotifier extends AsyncNotifier<LlmChatState> {
  final Logger _logger = Logger();

  @override
  Future<LlmChatState> build() async {
    final userId = ref.read(userStateNotifierProvider).user.idOrNull;
    if (userId == null) return const LlmChatState(messages: []);

    try {
      final entities = await ref.read(llmChatApiProvider).loadMessages(userId);
      return LlmChatState(
        messages: entities.map(ChatMessage.fromEntity).toList(),
      );
    } catch (e) {
      _logger.e('Failed to load LLM chat history: $e');
      // Graceful fallback: start fresh if the backend is unavailable.
      return const LlmChatState(messages: []);
    }
  }

  /// Adds the user message to the conversation, persists it, then streams
  /// the assistant reply chunk-by-chunk via SSE, updating state on each token.
  Future<void> sendMessage(String prompt) async {
    final current = switch (state) {
      AsyncData(:final value) => value,
      _ => null,
    };
    if (current == null || current.isReplying) return;

    final userMsg = ChatMessage.user(prompt);
    state = AsyncData(
      current.copyWith(
        messages: [...current.messages, userMsg],
        isReplying: true,
        streamingStarted: false,
      ),
    );

    _persistMessage(userMsg);

    // Pass only the last _kMaxContextMessages messages to the AI.
    // The full history is still stored in the DB for the user to read.
    final allMessages = (state as AsyncData<LlmChatState>).value.messages;
    final history = allMessages.length > _kMaxContextMessages
        ? allMessages.sublist(allMessages.length - _kMaxContextMessages)
        : allMessages;
    await _requestReplyStream(history);
  }

  // ---------------------------------------------------------------------------
  // SSE streaming
  // ---------------------------------------------------------------------------

  Future<void> _requestReplyStream(List<ChatMessage> history) async {
    final t = ref.read(translationsProvider).llm_chat;
    final String llmChatEndpoint = AppEnv.llmChatEndpoint;

    if (llmChatEndpoint.isEmpty) {
      _finalizeAssistantMessage(t.error_not_configured);
      return;
    }

    final idToken = await FirebaseAuth.instance.currentUser?.getIdToken();
    if (idToken == null) {
      _finalizeAssistantMessage(t.error_not_configured);
      return;
    }

    final dio = Dio(
      BaseOptions(
        headers: {
          'Authorization': 'Bearer $idToken',
          'Content-Type': 'application/json',
        },
      ),
    );

    try {
      final response = await dio.post<ResponseBody>(
        llmChatEndpoint,
        data: {
          'message': history.last.content,
          'history': history
              .map((e) => {'role': e.role, 'content': e.content})
              .toList(growable: false),
        },
        options: Options(responseType: ResponseType.stream),
      );

      final lineBuffer = StringBuffer();
      final contentBuffer = StringBuffer();

      await for (final chunk in response.data!.stream) {
        final text = utf8.decode(chunk, allowMalformed: true);
        lineBuffer.write(text);

        // Process only complete lines — keep the last incomplete fragment
        // in the buffer for the next iteration.
        final raw = lineBuffer.toString();
        final lastNewline = raw.lastIndexOf('\n');
        if (lastNewline == -1) continue;

        final completeSection = raw.substring(0, lastNewline + 1);
        lineBuffer.clear();
        if (lastNewline < raw.length - 1) {
          lineBuffer.write(raw.substring(lastNewline + 1));
        }

        for (final line in completeSection.split('\n')) {
          final delta = _extractSSEDelta(line.trimRight());
          if (delta.isEmpty) continue;

          contentBuffer.write(delta);
          _appendStreamingChunk(contentBuffer.toString());
        }
      }

      // Persist the complete assistant reply once the stream ends.
      final fullContent = contentBuffer.toString();
      if (fullContent.isNotEmpty) {
        _persistMessage(ChatMessage.assistant(fullContent));
      } else {
        // Stream ended with no content (e.g. error event)
        _finalizeAssistantMessage(t.error_no_reply);
        return;
      }

      // Mark the reply as finished without touching the message list.
      final latest = switch (state) {
        AsyncData(:final value) => value,
        _ => null,
      };
      if (latest != null) {
        state = AsyncData(
          latest.copyWith(isReplying: false, streamingStarted: false),
        );
      }
    } catch (error) {
      _logger.e('LLM chat stream failed: $error');
      _finalizeAssistantMessage(t.error_network);
    }
  }

  /// Called on every incoming SSE token to update the last assistant bubble.
  void _appendStreamingChunk(String partialContent) {
    final current = switch (state) {
      AsyncData(:final value) => value,
      _ => null,
    };
    if (current == null) return;

    final msgs = current.messages;
    final newMsgs = current.streamingStarted
        // Replace the in-progress bubble with the latest accumulated content.
        ? [
            ...msgs.sublist(0, msgs.length - 1),
            ChatMessage.assistant(partialContent),
          ]
        // First chunk: append a new assistant bubble.
        : [...msgs, ChatMessage.assistant(partialContent)];

    state = AsyncData(
      LlmChatState(messages: newMsgs, isReplying: true, streamingStarted: true),
    );
  }

  /// Replaces (or appends) the assistant bubble with [content] and marks
  /// the reply as finished. Used for error messages and empty-stream fallback.
  void _finalizeAssistantMessage(String content) {
    final current = switch (state) {
      AsyncData(:final value) => value,
      _ => const LlmChatState(messages: []),
    };
    final msgs = current.messages;
    final newMsgs = current.streamingStarted
        ? [...msgs.sublist(0, msgs.length - 1), ChatMessage.assistant(content)]
        : [...msgs, ChatMessage.assistant(content)];
    state = AsyncData(LlmChatState(messages: newMsgs));
  }

  // ---------------------------------------------------------------------------
  // SSE parsing
  // ---------------------------------------------------------------------------

  /// Extracts the text delta from a single SSE `data:` line.
  /// Supports both OpenAI (`choices[0].delta.content`) and
  /// Gemini (`candidates[0].content.parts[0].text`) formats.
  String _extractSSEDelta(String line) {
    if (!line.startsWith('data: ')) return '';
    final payload = line.substring(6);
    if (payload == '[DONE]') return '';
    try {
      final json = jsonDecode(payload) as Map<String, dynamic>;

      // OpenAI format
      final choices = json['choices'] as List?;
      if (choices != null && choices.isNotEmpty) {
        final delta =
            (choices.first as Map<String, dynamic>)['delta']
                as Map<String, dynamic>?;
        return (delta?['content'] as String?) ?? '';
      }

      // Gemini format
      final candidates = json['candidates'] as List?;
      if (candidates != null && candidates.isNotEmpty) {
        final content =
            (candidates.first as Map<String, dynamic>)['content']
                as Map<String, dynamic>?;
        final parts = content?['parts'] as List?;
        if (parts != null && parts.isNotEmpty) {
          return ((parts.first as Map<String, dynamic>)['text'] as String?) ??
              '';
        }
      }
    } catch (_) {}
    return '';
  }

  // ---------------------------------------------------------------------------
  // Persistence
  // ---------------------------------------------------------------------------

  void _persistMessage(ChatMessage message) {
    final userId = ref.read(userStateNotifierProvider).user.idOrNull;
    if (userId == null) return;
    ref
        .read(llmChatApiProvider)
        .saveMessage(userId, message.toEntity())
        .catchError((e) {
          _logger.e('Failed to persist message: $e');
        });
  }
}
