import 'dart:async';
import 'dart:convert';

import 'package:computer/computer.dart';
import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter_gbk2utf8/flutter_gbk2utf8.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:server_box/core/extension/ssh_client.dart';
import 'package:server_box/core/utils/server.dart';
import 'package:server_box/core/utils/ssh_auth.dart';
import 'package:server_box/data/helper/system_detector.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/app/scripts/script_consts.dart';
import 'package:server_box/data/model/app/scripts/shell_func.dart';
import 'package:server_box/data/model/server/connection_stat.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/server_status_update_req.dart';
import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/model/server/try_limiter.dart';
import 'package:server_box/data/provider/server/all.dart';
import 'package:server_box/data/res/status.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/data/ssh/session_manager.dart';

part 'single.g.dart';
part 'single.freezed.dart';

// Individual server state, including connection and status information
@freezed
abstract class ServerState with _$ServerState {
  const factory ServerState({
    required Spi spi,
    required ServerStatus status,
    @Default(ServerConn.disconnected) ServerConn conn,
    SSHClient? client,
    Future<void>? updateFuture,
  }) = _ServerState;
}

// Individual server state management
@Riverpod(keepAlive: true)
class ServerNotifier extends _$ServerNotifier {
  @override
  ServerState build(String serverId) {
    final serverNotifier = ref.read(serversProvider);
    final spi = serverNotifier.servers[serverId];
    if (spi == null) {
      throw StateError('Server $serverId not found');
    }

    return ServerState(spi: spi, status: InitStatus.status);
  }

  // Update connection status
  void updateConnection(ServerConn conn) {
    state = state.copyWith(conn: conn);
  }

  // Update server status
  void updateStatus(ServerStatus status) {
    state = state.copyWith(status: status);
  }

  // Update SSH client
  void updateClient(SSHClient? client) {
    state = state.copyWith(client: client);
  }

  // Update SPI configuration
  void updateSpi(Spi spi) {
    state = state.copyWith(spi: spi);
  }

  // Close connection
  void closeConnection() {
    final client = state.client;
    client?.close();
    state = state.copyWith(client: null, conn: ServerConn.disconnected);
  }

  // Refresh server status
  Future<void> refresh() async {
    if (state.updateFuture != null) {
      await state.updateFuture;
      return;
    }

    final updateFuture = _updateServer();
    state = state.copyWith(updateFuture: updateFuture);

    try {
      await updateFuture;
    } finally {
      state = state.copyWith(updateFuture: null);
    }
  }

  Future<void> _updateServer() async {
    await _getData();
  }

  Future<void> _getData() async {
    final spi = state.spi;
    final sid = spi.id;

    if (!TryLimiter.canTry(sid)) {
      if (state.conn != ServerConn.failed) {
        updateConnection(ServerConn.failed);
      }
      return;
    }

    final newStatus = state.status..err = null; // Clear previous error
    updateStatus(newStatus);

    if (state.conn < ServerConn.connecting || (state.client?.isClosed ?? true)) {
      updateConnection(ServerConn.connecting);

      // Wake on LAN
      final wol = spi.wolCfg;
      if (wol != null) {
        try {
          await wol.wake();
        } catch (e) {
          Loggers.app.warning('Wake on lan failed', e);
        }
      }

      try {
        final time1 = DateTime.now();
        final client = await genClient(
          spi,
          timeout: Duration(seconds: Stores.setting.timeout.fetch()),
          onKeyboardInteractive: (_) => KeybordInteractive.defaultHandle(spi),
        );
        updateClient(client);

        final time2 = DateTime.now();
        final spentTime = time2.difference(time1).inMilliseconds;
        if (spi.jumpId == null) {
          Loggers.app.info('Connected to ${spi.name} in $spentTime ms.');
        } else {
          Loggers.app.info('Jump to ${spi.name} in $spentTime ms.');
        }

        // Record successful connection
        Stores.connectionStats.recordConnection(ConnectionStat(
          serverId: spi.id,
          serverName: spi.name,
          timestamp: time1,
          result: ConnectionResult.success,
          durationMs: spentTime,
        ));

        final sessionId = 'ssh_${spi.id}';
        TermSessionManager.add(
          id: sessionId,
          spi: spi,
          startTimeMs: time1.millisecondsSinceEpoch,
          disconnect: () => ref.read(serversProvider.notifier).closeOneServer(spi.id),
          status: TermSessionStatus.connecting,
        );
        TermSessionManager.setActive(sessionId, hasTerminal: false);
      } catch (e) {
        TryLimiter.inc(sid);
        
        // Determine connection failure type
        ConnectionResult failureResult;
        if (e.toString().contains('timeout') || e.toString().contains('Timeout')) {
          failureResult = ConnectionResult.timeout;
        } else if (e.toString().contains('auth') || e.toString().contains('Authentication')) {
          failureResult = ConnectionResult.authFailed;
        } else if (e.toString().contains('network') || e.toString().contains('Network')) {
          failureResult = ConnectionResult.networkError;
        } else {
          failureResult = ConnectionResult.unknownError;
        }
        
        // Record failed connection
        Stores.connectionStats.recordConnection(ConnectionStat(
          serverId: spi.id,
          serverName: spi.name,
          timestamp: DateTime.now(),
          result: failureResult,
          errorMessage: e.toString(),
          durationMs: 0,
        ));
        
        final newStatus = state.status..err = SSHErr(type: SSHErrType.connect, message: e.toString());
        updateStatus(newStatus);
        updateConnection(ServerConn.failed);

        // Remove SSH session when connection fails
        final sessionId = 'ssh_${spi.id}';
        TermSessionManager.remove(sessionId);

        Loggers.app.warning('Connect to ${spi.name} failed', e);
        return;
      }

      updateConnection(ServerConn.connected);

      // Update SSH session status to connected
      final sessionId = 'ssh_${spi.id}';
      TermSessionManager.updateStatus(sessionId, TermSessionStatus.connected);

      try {
        // Detect system type
        final detectedSystemType = await SystemDetector.detect(state.client!, spi);
        final newStatus = state.status..system = detectedSystemType;
        updateStatus(newStatus);

        final (_, writeScriptResult) = await state.client!.exec(
          (session) async {
            final scriptRaw = ShellFuncManager.allScript(
              spi.custom?.cmds,
              systemType: detectedSystemType,
              disabledCmdTypes: spi.disabledCmdTypes,
            ).uint8List;
            session.stdin.add(scriptRaw);
            session.stdin.close();
          },
          entry: ShellFuncManager.getInstallShellCmd(
            spi.id,
            systemType: detectedSystemType,
            customDir: spi.custom?.scriptDir,
          ),
        );
        if (writeScriptResult.isNotEmpty && detectedSystemType != SystemType.windows) {
          ShellFuncManager.switchScriptDir(spi.id, systemType: detectedSystemType);
          throw writeScriptResult;
        }
      } on SSHAuthAbortError catch (e) {
        TryLimiter.inc(sid);
        final err = SSHErr(type: SSHErrType.auth, message: e.toString());
        final newStatus = state.status..err = err;
        updateStatus(newStatus);
        Loggers.app.warning(err);
        updateConnection(ServerConn.failed);

        final sessionId = 'ssh_${spi.id}';
        TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
        return;
      } on SSHAuthFailError catch (e) {
        TryLimiter.inc(sid);
        final err = SSHErr(type: SSHErrType.auth, message: e.toString());
        final newStatus = state.status..err = err;
        updateStatus(newStatus);
        Loggers.app.warning(err);
        updateConnection(ServerConn.failed);

        final sessionId = 'ssh_${spi.id}';
        TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
        return;
      } catch (e) {
        final err = SSHErr(type: SSHErrType.writeScript, message: e.toString());
        final newStatus = state.status..err = err;
        updateStatus(newStatus);
        Loggers.app.warning(err);
        updateConnection(ServerConn.failed);

        final sessionId = 'ssh_${spi.id}';
        TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
      }
    }

    if (state.conn == ServerConn.connecting) return;

    // Keep finished status to prevent UI from refreshing to loading state
    if (state.conn != ServerConn.finished) {
      updateConnection(ServerConn.loading);
    }

    List<String>? segments;
    String? raw;

    try {
      final execResult = await state.client?.run(
        ShellFunc.status.exec(spi.id, systemType: state.status.system, customDir: spi.custom?.scriptDir),
      );
      if (execResult != null) {
        String? rawStr;
        bool needGbk = false;
        try {
          rawStr = utf8.decode(execResult, allowMalformed: true);
          // If there are unparseable characters, try fallback to GBK decoding
          if (rawStr.contains('�')) {
            Loggers.app.warning('UTF8 decoding failed, use GBK decoding');
            needGbk = true;
          }
        } catch (e) {
          Loggers.app.warning('UTF8 decoding failed, use GBK decoding', e);
          needGbk = true;
        }
        if (needGbk) {
          try {
            rawStr = gbk.decode(execResult);
          } catch (e2) {
            Loggers.app.warning('GBK decoding failed', e2);
            rawStr = null;
          }
        }
        if (rawStr == null) {
          Loggers.app.warning('Decoding failed, execResult: $execResult');
        }
        raw = rawStr;
      } else {
        raw = execResult.toString();
      }

      if (raw == null || raw.isEmpty) {
        TryLimiter.inc(sid);
        final newStatus = state.status
          ..err = SSHErr(type: SSHErrType.segements, message: 'decode or split failed, raw:\n$raw');
        updateStatus(newStatus);
        updateConnection(ServerConn.failed);

        final sessionId = 'ssh_${spi.id}';
        TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
        return;
      }

      segments = raw.split(ScriptConstants.separator).map((e) => e.trim()).toList();
      if (raw.isEmpty || segments.isEmpty) {
        if (Stores.setting.keepStatusWhenErr.fetch()) {
          // Keep previous server status when error occurs
          if (state.conn != ServerConn.failed && state.status.more.isNotEmpty) {
            return;
          }
        }
        TryLimiter.inc(sid);
        final newStatus = state.status
          ..err = SSHErr(type: SSHErrType.segements, message: 'Seperate segments failed, raw:\n$raw');
        updateStatus(newStatus);
        updateConnection(ServerConn.failed);

        final sessionId = 'ssh_${spi.id}';
        TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
        return;
      }
    } catch (e) {
      TryLimiter.inc(sid);
      final newStatus = state.status..err = SSHErr(type: SSHErrType.getStatus, message: e.toString());
      updateStatus(newStatus);
      updateConnection(ServerConn.failed);
      Loggers.app.warning('Get status from ${spi.name} failed', e);

      final sessionId = 'ssh_${spi.id}';
      TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
      return;
    }

    try {
      // Parse script output into command-specific mappings
      final parsedOutput = ScriptConstants.parseScriptOutput(raw);

      final req = ServerStatusUpdateReq(
        ss: state.status,
        parsedOutput: parsedOutput,
        system: state.status.system,
        customCmds: spi.custom?.cmds ?? {},
      );
      final newStatus = await Computer.shared.start(getStatus, req, taskName: 'StatusUpdateReq<${spi.id}>');
      updateStatus(newStatus);
    } catch (e, trace) {
      TryLimiter.inc(sid);
      final newStatus = state.status
        ..err = SSHErr(type: SSHErrType.getStatus, message: 'Parse failed: $e\n\n$raw');
      updateStatus(newStatus);
      updateConnection(ServerConn.failed);
      Loggers.app.warning('Server status', e, trace);

      final sessionId = 'ssh_${spi.id}';
      TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
      return;
    }

    // Set Server.isBusy to false each time this method is called
    updateConnection(ServerConn.finished);
    // Reset retry count only after successful preparation
    TryLimiter.reset(sid);
  }
}

extension IndividualServerStateExtension on ServerState {
  bool get needGenClient => conn < ServerConn.connecting;

  bool get canViewDetails => conn == ServerConn.finished;

  String get id => spi.id;
}
