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

import 'package:http/http.dart' as http;
import 'package:oto/oto/agent/agent_config.dart';
import 'package:oto/oto/agent/oto_server_job_client.dart';
import 'package:oto/oto/agent/oto/runner.pb.dart' as oto;
import 'package:oto/oto/agent/runner_capability_provider.dart';
import 'package:protobuf/protobuf.dart';
import 'package:proto_socket/proto_socket.dart';

const otoRunnerProtocolVersion = 'oto.runner.v1';
const otoRunnerCapabilityName = 'oto-runner';
const otoRunnerCapabilityVersion = '1.0.0';

class RegistrationResult {
  final bool accepted;
  final String? rejectReason;
  final String? nodeId;
  final String? alias;
  final Map<String, dynamic>? runtimeConfig;

  RegistrationResult._({
    required this.accepted,
    this.rejectReason,
    this.nodeId,
    this.alias,
    this.runtimeConfig,
  });

  String? get runnerId => nodeId;

  factory RegistrationResult.accepted(
    String nodeId,
    String? alias,
    Map<String, dynamic> runtimeConfig,
  ) {
    return RegistrationResult._(
      accepted: true,
      nodeId: nodeId,
      alias: alias,
      runtimeConfig: runtimeConfig,
    );
  }

  factory RegistrationResult.rejected(String reason) {
    return RegistrationResult._(accepted: false, rejectReason: reason);
  }

  factory RegistrationResult.fromOtoResponse(
    oto.RegisterRunnerResponse response,
  ) {
    if (!response.accepted) {
      final reason = response.rejectReason.isNotEmpty
          ? response.rejectReason
          : (response.hasError() ? response.error.message : 'rejected');
      return RegistrationResult.rejected(reason);
    }

    return RegistrationResult.accepted(
      response.runnerId,
      response.alias.isEmpty ? null : response.alias,
      const {},
    );
  }

  factory RegistrationResult.fromOtoJson(Map<String, dynamic> json) {
    final accepted = json['accepted'] == true;
    if (!accepted) {
      final err = json['error'];
      final errMessageFromObj = (err is Map) ? err['message'] : null;
      return RegistrationResult.rejected(
        (json['reject_reason'] ??
                json['rejectReason'] ??
                errMessageFromObj ??
                'rejected')
            .toString(),
      );
    }
    return RegistrationResult.accepted(
      (json['runner_id'] ?? json['runnerId'] ?? '').toString(),
      _emptyToNull((json['alias'] ?? '').toString()),
      const {},
    );
  }

  static String? _emptyToNull(String value) => value.isEmpty ? null : value;
}

/// OTO Server 등록 세션의 기본 인터페이스.
///
/// 세션 소유자는 종료 시 [close]를 호출해 transport를 정리할 책임이 있다.
abstract class AgentSession {
  RegistrationResult get result;
  Future<void> close();
}

abstract class OtoServerJobSession implements AgentSession {
  OtoServerJobClient get jobs;
}

abstract class OtoServerPushJobSession implements AgentSession {
  RemoteJobReporter get jobReporter;
  Stream<oto.RunRequest> get runRequests;
  Stream<oto.CancelRunRequest> get cancelRequests;
}

abstract class RegistrationClient {
  Future<AgentSession> openSession(AgentConfig config);

  Future<RegistrationResult> register(AgentConfig config) async {
    final session = await openSession(config);
    try {
      return session.result;
    } finally {
      await session.close();
    }
  }
}

class OtoServerSocketRegistrationClient extends RegistrationClient {
  final RunnerCapabilityProvider _capabilityProvider;
  final Duration _heartbeatInterval;

  OtoServerSocketRegistrationClient({
    Iterable<String>? commandTypes,
    RunnerCapabilityProvider? capabilityProvider,
    Duration heartbeatInterval = const Duration(seconds: 30),
  }) : _capabilityProvider =
           capabilityProvider ??
           _StaticRunnerCapabilityProvider(commandTypes ?? const []),
       _heartbeatInterval = heartbeatInterval;

  @override
  Future<AgentSession> openSession(AgentConfig config) async {
    final endpoint = _RunnerSocketEndpoint.fromConfig(config.server);
    final socket = await ProtobufClient.connect(
      endpoint.host,
      endpoint.port,
    ).timeout(const Duration(seconds: 5));
    final client = _OtoRunnerSocketClient(socket);
    try {
      final response = await client
          .sendRequest<oto.RegisterRunnerRequest, oto.RegisterRunnerResponse>(
            buildRegisterRunnerRequest(config),
            timeout: const Duration(seconds: 5),
          );
      final result = RegistrationResult.fromOtoResponse(response);
      final runnerId = result.runnerId ?? config.agent.id;
      return _OtoServerSocketAgentSession(
        result,
        client,
        runnerId,
        heartbeatInterval: _heartbeatInterval,
      );
    } catch (_) {
      await client.close();
      rethrow;
    }
  }

  oto.RegisterRunnerRequest buildRegisterRunnerRequest(AgentConfig config) {
    final capability = _capabilityProvider.snapshot();
    return oto.RegisterRunnerRequest()
      ..enrollmentToken = config.agent.enrollmentToken
      ..runnerId = config.agent.id
      ..alias = config.agent.alias ?? ''
      ..protocolVersion = otoRunnerProtocolVersion
      ..capability = (oto.RunnerCapability()
        ..name = capability.name
        ..version = capability.version)
      ..commandCatalog = (oto.CommandCatalogSummary()
        ..commandTypes.addAll(capability.commandTypes));
  }
}

/// Compatibility fallback HTTP registration client for legacy runner registration.
class OtoServerRegistrationClient extends RegistrationClient {
  final http.Client? _client;
  final RunnerCapabilityProvider _capabilityProvider;
  final Duration _heartbeatInterval;

  OtoServerRegistrationClient({
    http.Client? client,
    Iterable<String>? commandTypes,
    RunnerCapabilityProvider? capabilityProvider,
    Duration heartbeatInterval = const Duration(seconds: 30),
  }) : _client = client,
       _capabilityProvider =
           capabilityProvider ??
           _StaticRunnerCapabilityProvider(commandTypes ?? const []),
       _heartbeatInterval = heartbeatInterval;

  @override
  Future<AgentSession> openSession(AgentConfig config) async {
    final client = _client ?? http.Client();
    final shouldCloseClient = _client == null;
    try {
      final request = buildRegisterRunnerRequest(config);
      final response = await client
          .post(
            _registerUri(config.server.url),
            headers: {'content-type': 'application/json'},
            body: jsonEncode(_registerRequestJson(request)),
          )
          .timeout(const Duration(seconds: 5));
      if (response.statusCode < 200 || response.statusCode >= 300) {
        throw HttpException(
          'Registration request failed with HTTP ${response.statusCode}',
        );
      }
      final decoded = jsonDecode(response.body);
      if (decoded is! Map<String, dynamic>) {
        throw const FormatException('Invalid registration response body');
      }
      final result = RegistrationResult.fromOtoJson(decoded);
      final runnerId = result.runnerId ?? config.agent.id;
      return _OtoServerAgentSession(
        result,
        client,
        shouldCloseClient,
        config.server.url,
        runnerId,
        heartbeatInterval: _heartbeatInterval,
      );
    } catch (_) {
      if (shouldCloseClient) {
        client.close();
      }
      rethrow;
    }
  }

  oto.RegisterRunnerRequest buildRegisterRunnerRequest(AgentConfig config) {
    final capability = _capabilityProvider.snapshot();
    final request = oto.RegisterRunnerRequest()
      ..enrollmentToken = config.agent.enrollmentToken
      ..runnerId = config.agent.id
      ..alias = config.agent.alias ?? ''
      ..protocolVersion = otoRunnerProtocolVersion
      ..capability = (oto.RunnerCapability()
        ..name = capability.name
        ..version = capability.version)
      ..commandCatalog = (oto.CommandCatalogSummary()
        ..commandTypes.addAll(capability.commandTypes));
    return request;
  }

  static Uri _registerUri(String serverUrl) {
    var raw = serverUrl;
    if (!raw.contains('://')) {
      raw = 'http://$raw';
    }
    final uri = Uri.parse(raw);
    if (uri.host.isEmpty) {
      throw FormatException('Invalid server url: "$serverUrl"');
    }
    return uri.replace(path: _joinPath(uri.path, '/api/v1/runners/register'));
  }

  static Uri heartbeatUri(String serverUrl, String runnerId) {
    var raw = serverUrl;
    if (!raw.contains('://')) {
      raw = 'http://$raw';
    }
    final uri = Uri.parse(raw);
    if (uri.host.isEmpty) {
      throw FormatException('Invalid server url: "$serverUrl"');
    }
    return uri.replace(
      path: _joinPath(uri.path, '/api/v1/runners/$runnerId/heartbeat'),
    );
  }

  static Uri disconnectUri(String serverUrl, String runnerId) {
    var raw = serverUrl;
    if (!raw.contains('://')) {
      raw = 'http://$raw';
    }
    final uri = Uri.parse(raw);
    if (uri.host.isEmpty) {
      throw FormatException('Invalid server url: "$serverUrl"');
    }
    return uri.replace(
      path: _joinPath(uri.path, '/api/v1/runners/$runnerId/disconnect'),
    );
  }

  static String _joinPath(String basePath, String endpointPath) {
    final base = basePath.endsWith('/')
        ? basePath.substring(0, basePath.length - 1)
        : basePath;
    return '$base$endpointPath';
  }

  static Map<String, dynamic> _registerRequestJson(
    oto.RegisterRunnerRequest request,
  ) => {
    'enrollment_token': request.enrollmentToken,
    'runner_id': request.runnerId,
    'alias': request.alias,
    'protocol_version': request.protocolVersion,
    'capability': {
      'name': request.capability.name,
      'version': request.capability.version,
    },
    'command_catalog': {
      'command_types': request.commandCatalog.commandTypes.toList(),
    },
  };
}

class _OtoRunnerSocketClient extends ProtobufClient {
  _OtoRunnerSocketClient(Socket socket)
    : super(socket, 30, 45, _otoRunnerSocketParserMap());
}

class _OtoServerSocketAgentSession implements OtoServerPushJobSession {
  final _OtoRunnerSocketClient _client;
  final String _runnerId;
  final StreamController<oto.RunRequest> _runRequests =
      StreamController<oto.RunRequest>();
  final StreamController<oto.CancelRunRequest> _cancelRequests =
      StreamController<oto.CancelRunRequest>();
  Timer? _heartbeatTimer;
  bool _closed = false;

  @override
  final RegistrationResult result;

  @override
  late final RemoteJobReporter jobReporter;

  @override
  Stream<oto.RunRequest> get runRequests => _runRequests.stream;

  @override
  Stream<oto.CancelRunRequest> get cancelRequests => _cancelRequests.stream;

  _OtoServerSocketAgentSession(
    this.result,
    this._client,
    this._runnerId, {
    required Duration heartbeatInterval,
  }) {
    jobReporter = OtoServerSocketJobClient(
      runnerId: _runnerId,
      socket: _client,
    );
    _client.addListener<oto.RunRequest>((request) {
      if (!_runRequests.isClosed) {
        _runRequests.add(request);
      }
    });
    _client.addListener<oto.CancelRunRequest>((request) {
      if (!_cancelRequests.isClosed) {
        _cancelRequests.add(request);
      }
    });
    if (result.accepted) {
      _heartbeatTimer = Timer.periodic(
        heartbeatInterval,
        (_) => _sendHeartbeat(),
      );
      scheduleMicrotask(() => _sendHeartbeat());
    }
  }

  Future<void> _sendHeartbeat() async {
    if (_closed) return;
    try {
      await _client.sendRequest<oto.HeartbeatRequest, oto.HeartbeatResponse>(
        oto.HeartbeatRequest()
          ..runnerId = _runnerId
          ..status = oto.HeartbeatStatus.HEARTBEAT_STATUS_HEALTHY,
        timeout: const Duration(seconds: 5),
      );
    } catch (_) {
      // Ignore background heartbeat errors; proto-socket heartbeat owns liveness.
    }
  }

  @override
  Future<void> close() async {
    if (_closed) return;
    _closed = true;
    _heartbeatTimer?.cancel();
    _heartbeatTimer = null;
    unawaited(_runRequests.close());
    unawaited(_cancelRequests.close());
    await _client.close();
  }
}

class _RunnerSocketEndpoint {
  final String host;
  final int port;

  const _RunnerSocketEndpoint({required this.host, required this.port});

  factory _RunnerSocketEndpoint.fromConfig(ServerConnectionConfig config) {
    final raw = config.socketUrl ?? _deriveSocketUrl(config.url);
    var parsedRaw = raw;
    if (!parsedRaw.contains('://')) {
      parsedRaw = 'tcp://$parsedRaw';
    }
    final uri = Uri.parse(parsedRaw);
    if (uri.host.isEmpty) {
      throw FormatException('Invalid runner socket url: "$raw"');
    }
    return _RunnerSocketEndpoint(
      host: uri.host,
      port: uri.hasPort ? uri.port : 18080,
    );
  }

  static String _deriveSocketUrl(String serverUrl) {
    var raw = serverUrl;
    if (!raw.contains('://')) {
      raw = 'http://$raw';
    }
    final uri = Uri.parse(raw);
    if (uri.host.isEmpty) {
      throw FormatException('Invalid server url: "$serverUrl"');
    }
    return 'tcp://${uri.host}:18080';
  }
}

Map<String, GeneratedMessage Function(List<int>)> _otoRunnerSocketParserMap() {
  return {
    oto.RegisterRunnerResponse.getDefault().info_.qualifiedMessageName:
        oto.RegisterRunnerResponse.fromBuffer,
    oto.HeartbeatResponse.getDefault().info_.qualifiedMessageName:
        oto.HeartbeatResponse.fromBuffer,
    oto.RunRequest.getDefault().info_.qualifiedMessageName:
        oto.RunRequest.fromBuffer,
    oto.ExecutionReportResponse.getDefault().info_.qualifiedMessageName:
        oto.ExecutionReportResponse.fromBuffer,
    oto.LogAppendResponse.getDefault().info_.qualifiedMessageName:
        oto.LogAppendResponse.fromBuffer,
    oto.ArtifactReportResponse.getDefault().info_.qualifiedMessageName:
        oto.ArtifactReportResponse.fromBuffer,
    oto.CancelRunRequest.getDefault().info_.qualifiedMessageName:
        oto.CancelRunRequest.fromBuffer,
    oto.SelfUpdateRequest.getDefault().info_.qualifiedMessageName:
        oto.SelfUpdateRequest.fromBuffer,
  };
}

class _StaticRunnerCapabilityProvider implements RunnerCapabilityProvider {
  final List<String> _commandTypes;

  _StaticRunnerCapabilityProvider(Iterable<String> commandTypes)
    : _commandTypes = List.unmodifiable(commandTypes);

  @override
  RunnerCapabilitySnapshot snapshot() {
    return RunnerCapabilitySnapshot(
      name: otoRunnerCapabilityName,
      version: otoRunnerCapabilityVersion,
      commandTypes: _commandTypes,
    );
  }
}

/// Compatibility fallback HTTP agent session.
class _OtoServerAgentSession implements OtoServerJobSession {
  final http.Client _client;
  final bool _shouldCloseClient;
  final String _serverUrl;
  final String _runnerId;
  Timer? _heartbeatTimer;
  bool _closed = false;

  @override
  final RegistrationResult result;

  @override
  late final OtoServerJobClient jobs;

  _OtoServerAgentSession(
    this.result,
    this._client,
    this._shouldCloseClient,
    this._serverUrl,
    this._runnerId, {
    required Duration heartbeatInterval,
  }) {
    jobs = OtoServerJobClient(
      serverUrl: _serverUrl,
      runnerId: _runnerId,
      client: _client,
    );
    if (result.accepted) {
      _heartbeatTimer = Timer.periodic(
        heartbeatInterval,
        (_) => _sendHeartbeat(),
      );
      scheduleMicrotask(() => _sendHeartbeat());
    }
  }

  Future<void> _sendHeartbeat() async {
    if (_closed) return;
    try {
      final uri = OtoServerRegistrationClient.heartbeatUri(
        _serverUrl,
        _runnerId,
      );
      final body = jsonEncode({
        'runner_id': _runnerId,
        'status': 1, // HEARTBEAT_STATUS_HEALTHY
      });
      await _client
          .post(uri, headers: {'content-type': 'application/json'}, body: body)
          .timeout(const Duration(seconds: 5));
    } catch (_) {
      // Ignore background errors
    }
  }

  @override
  Future<void> close() async {
    if (_closed) return;
    _closed = true;

    _heartbeatTimer?.cancel();
    _heartbeatTimer = null;

    if (result.accepted) {
      try {
        final uri = OtoServerRegistrationClient.disconnectUri(
          _serverUrl,
          _runnerId,
        );
        await _client
            .post(uri, headers: {'content-type': 'application/json'})
            .timeout(const Duration(seconds: 5));
      } catch (_) {
        // Ignore disconnect errors
      }
    }

    if (_shouldCloseClient) {
      _client.close();
    }
  }
}
