import 'package:flutter/material.dart';
import 'package:oto_console/oto_console.dart';

import 'core_connection_client.dart';

const otoDefaultCapabilityPack = OtoCapabilityPack(
  capabilities: [
    OtoCapability(
      name: 'Runner Registry',
      description: 'Track connected build runners and bootstrap status.',
    ),
    OtoCapability(
      name: 'Remote Run',
      description: 'Dispatch typed command build and deploy pipelines.',
    ),
    OtoCapability(
      name: 'Execution Status',
      description: 'Follow active runs, cancellation, and terminal state.',
    ),
    OtoCapability(
      name: 'Step Logs',
      description: 'Inspect live command output and execution traces.',
    ),
    OtoCapability(
      name: 'Artifacts',
      description: 'Review build output events and published artifacts.',
    ),
  ],
);

class OtoClientApp extends StatefulWidget {
  final OtoConsoleConfig config;
  final OtoCoreConnectionSnapshot initialConnection;
  final OtoCoreConnectionClient? coreClient;
  final OtoCoreReadClient? readClient;
  final OtoCoreWriteClient? writeClient;
  final List<String> runnerIDs;
  final List<String> jobIDs;
  final List<String> executionIDs;
  final OtoCapabilityPack capabilities;

  const OtoClientApp({
    super.key,
    this.config = const OtoConsoleConfig(
      serverHttpUrl: String.fromEnvironment(
        'OTO_SERVER_HTTP_URL',
        defaultValue: 'http://localhost:8080',
      ),
      serverWireUrl: String.fromEnvironment(
        'OTO_SERVER_WIRE_URL',
        defaultValue: 'ws://localhost:18080/runner',
      ),
    ),
    this.initialConnection = const OtoCoreConnectionSnapshot.checking(),
    this.coreClient,
    this.readClient,
    this.writeClient,
    this.runnerIDs = const [],
    this.jobIDs = const [],
    this.executionIDs = const [],
    this.capabilities = otoDefaultCapabilityPack,
  });

  @override
  State<OtoClientApp> createState() => _OtoClientAppState();
}

class _OtoClientAppState extends State<OtoClientApp> {
  late final OtoCoreConnectionClient _defaultCoreClient =
      OtoHttpCoreConnectionClient();
  late final OtoCoreReadClient _defaultReadClient = OtoHttpCoreReadClient();
  late final OtoCoreWriteClient _defaultWriteClient = OtoHttpCoreWriteClient();
  late OtoCoreConnectionSnapshot _connection = widget.initialConnection;
  OtoSurfaceSnapshot<List<OtoRunnerViewModel>> _runnersSnapshot =
      const OtoSurfaceSnapshot.loading();
  OtoSurfaceSnapshot<List<OtoJobViewModel>> _jobsSnapshot =
      const OtoSurfaceSnapshot.loading();
  OtoSurfaceSnapshot<List<OtoExecutionViewModel>> _executionsSnapshot =
      const OtoSurfaceSnapshot.loading();
  OtoSurfaceSnapshot<List<OtoArtifactViewModel>> _artifactsSnapshot =
      const OtoSurfaceSnapshot.loading();

  String? _expandedExecutionID;
  OtoSurfaceSnapshot<List<OtoLogEntryViewModel>>? _logsSnapshot;
  OtoSurfaceSnapshot<List<OtoArtifactViewModel>>? _expandedArtifactsSnapshot;

  OtoConsoleSection _activeSection = OtoConsoleSection.overview;
  int _refreshGeneration = 0;

  final Set<String> _createdJobIDs = <String>{};
  OtoActionViewState _jobCreateState = const OtoActionViewState.idle();
  bool _isSubmittingJob = false;
  final Map<String, OtoActionViewState> _executionActionStates = <String, OtoActionViewState>{};
  final Map<String, OtoRunnerActionState> _runnerActionStates = <String, OtoRunnerActionState>{};

  @override
  void initState() {
    super.initState();
    _refreshCoreStatus();
  }

  @override
  void didUpdateWidget(covariant OtoClientApp oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.config.serverHttpUrl != widget.config.serverHttpUrl ||
        oldWidget.coreClient != widget.coreClient ||
        oldWidget.readClient != widget.readClient ||
        oldWidget.writeClient != widget.writeClient ||
        !_listEquals(oldWidget.runnerIDs, widget.runnerIDs) ||
        !_listEquals(oldWidget.jobIDs, widget.jobIDs) ||
        !_listEquals(oldWidget.executionIDs, widget.executionIDs)) {
      _refreshCoreStatus();
    }
  }

  Future<void> _submitJobCreate(OtoJobCreateDraft draft) async {
    if (_isSubmittingJob) return;

    setState(() {
      _isSubmittingJob = true;
      _jobCreateState = const OtoActionViewState.submitting();
    });

    final writeClient = widget.writeClient ?? _defaultWriteClient;
    final result = await writeClient.createJob(widget.config, draft);

    if (!mounted) return;

    setState(() {
      _isSubmittingJob = false;
      if (result.isSuccess) {
        _createdJobIDs.add(result.data!.id);
        _jobCreateState = OtoActionViewState.succeeded(
          message: result.message,
        );
        _refreshCoreStatus();
      } else {
        _jobCreateState = OtoActionViewState.failed(message: result.message);
      }
    });
  }

  Future<void> _submitRunnerSelfUpdate(OtoRunnerSelfUpdateDraft draft) async {
    setState(() {
      _runnerActionStates[draft.runnerID] = const OtoActionViewState.submitting();
    });

    final writeClient = widget.writeClient ?? _defaultWriteClient;
    final result = await writeClient.selfUpdateRunner(widget.config, draft);

    if (!mounted) return;

    final accepted = result.state == OtoCoreWriteState.succeeded ||
        result.state == OtoCoreWriteState.deferred;

    setState(() {
      if (result.state == OtoCoreWriteState.deferred) {
        _runnerActionStates[draft.runnerID] = OtoActionViewState.deferred(
          message: result.message,
        );
      } else if (accepted) {
        _runnerActionStates[draft.runnerID] = OtoActionViewState.succeeded(
          message: result.message,
        );
      } else {
        _runnerActionStates[draft.runnerID] = OtoActionViewState.failed(
          message: result.message,
        );
      }
    });

    if (accepted) {
      _refreshCoreStatus();
    }
  }

  Future<void> _submitCancelExecution(OtoExecutionCancelDraft draft) async {
    setState(() {
      _executionActionStates[draft.executionID] = const OtoActionViewState.submitting();
    });

    final writeClient = widget.writeClient ?? _defaultWriteClient;
    final result = await writeClient.cancelExecution(widget.config, draft);

    if (!mounted) return;

    _handleExecutionActionResult(draft.executionID, result);
  }

  Future<void> _submitReportExecution(OtoExecutionReportDraft draft) async {
    setState(() {
      _executionActionStates[draft.executionID] = const OtoActionViewState.submitting();
    });

    final writeClient = widget.writeClient ?? _defaultWriteClient;
    final result = await writeClient.reportExecution(widget.config, draft);

    if (!mounted) return;

    _handleExecutionActionResult(draft.executionID, result);
  }

  Future<void> _submitAppendExecutionLog(OtoExecutionLogDraft draft) async {
    setState(() {
      _executionActionStates[draft.executionID] = const OtoActionViewState.submitting();
    });

    final writeClient = widget.writeClient ?? _defaultWriteClient;
    final result = await writeClient.appendExecutionLog(widget.config, draft);

    if (!mounted) return;

    _handleExecutionActionResult(draft.executionID, result);
  }

  Future<void> _submitAppendExecutionArtifact(OtoExecutionArtifactDraft draft) async {
    setState(() {
      _executionActionStates[draft.executionID] = const OtoActionViewState.submitting();
    });

    final writeClient = widget.writeClient ?? _defaultWriteClient;
    final result = await writeClient.appendExecutionArtifact(widget.config, draft);

    if (!mounted) return;

    _handleExecutionActionResult(draft.executionID, result);
  }

  void _handleExecutionActionResult(String executionID, OtoCoreWriteResult<void> result) {
    if (!mounted) return;

    // Execution actions return `OtoCoreWriteResult<void>.succeeded(null, ...)` or `.deferred(...)`.
    // `result.isSuccess` requires `data != null`, so check state directly.
    final accepted = result.state == OtoCoreWriteState.succeeded ||
        result.state == OtoCoreWriteState.deferred;

    if (accepted) {
      setState(() {
        _executionActionStates[executionID] = OtoActionViewState.succeeded(message: result.message);
      });
      _refreshCoreStatus();
      if (_expandedExecutionID == executionID) {
        _loadExecutionDetail(executionID);
      }
    } else {
      setState(() {
        _executionActionStates[executionID] = OtoActionViewState.failed(message: result.message);
      });
    }
  }

  bool _listEquals(List<String>? a, List<String>? b) {
    if (a == b) return true;
    if (a == null || b == null) return false;
    if (a.length != b.length) return false;
    for (int i = 0; i < a.length; i++) {
      if (a[i] != b[i]) return false;
    }
    return true;
  }

  Future<void> _refreshCoreStatus() async {
    final generation = ++_refreshGeneration;
    setState(() {
      _connection = const OtoCoreConnectionSnapshot.checking();
      _runnersSnapshot = const OtoSurfaceSnapshot.loading();
      _jobsSnapshot = const OtoSurfaceSnapshot.loading();
      _executionsSnapshot = const OtoSurfaceSnapshot.loading();
      _artifactsSnapshot = const OtoSurfaceSnapshot.loading();
    });

    final coreClient = widget.coreClient ?? _defaultCoreClient;
    final readClient = widget.readClient ?? _defaultReadClient;

    final Future<OtoCoreConnectionSnapshot> connectionFuture = coreClient.check(
      widget.config,
    );

    final Future<OtoSurfaceSnapshot<List<OtoRunnerViewModel>>> runnersFuture =
        _fetchRunners(readClient);

    final Future<OtoSurfaceSnapshot<List<OtoJobViewModel>>> jobsFuture =
        _fetchJobs(readClient);

    final Future<OtoSurfaceSnapshot<List<OtoExecutionViewModel>>> executionsFuture =
        _fetchExecutions(readClient);

    final Future<OtoSurfaceSnapshot<List<OtoArtifactViewModel>>> artifactsFuture =
        _fetchArtifacts(readClient);

    final results = await Future.wait([
      connectionFuture,
      runnersFuture,
      jobsFuture,
      executionsFuture,
      artifactsFuture,
    ]);
    final connection = results[0] as OtoCoreConnectionSnapshot;
    final runnersSnapshot =
        results[1] as OtoSurfaceSnapshot<List<OtoRunnerViewModel>>;
    final jobsSnapshot =
        results[2] as OtoSurfaceSnapshot<List<OtoJobViewModel>>;
    final executionsSnapshot =
        results[3] as OtoSurfaceSnapshot<List<OtoExecutionViewModel>>;
    final artifactsSnapshot =
        results[4] as OtoSurfaceSnapshot<List<OtoArtifactViewModel>>;

    if (!mounted || generation != _refreshGeneration) {
      return;
    }

    setState(() {
      _connection = connection;
      _runnersSnapshot = runnersSnapshot;
      _jobsSnapshot = jobsSnapshot;
      _executionsSnapshot = executionsSnapshot;
      _artifactsSnapshot = artifactsSnapshot;
    });

    if (_expandedExecutionID != null) {
      _loadExecutionDetail(_expandedExecutionID);
    }
  }

  Future<OtoSurfaceSnapshot<List<OtoRunnerViewModel>>> _fetchRunners(
    OtoCoreReadClient readClient,
  ) async {
    if (widget.runnerIDs.isEmpty) {
      return const OtoSurfaceSnapshot.empty();
    }

    try {
      final results = await Future.wait(
        widget.runnerIDs.map((id) async {
          final recordResult = await readClient.fetchRunner(widget.config, id);
          final statusResult = await readClient.fetchRunnerStatus(
            widget.config,
            id,
          );
          return _RunnerFetchResult(id, recordResult, statusResult);
        }),
      );

      final viewModels = <OtoRunnerViewModel>[];
      String? errorMessage;
      bool isLoading = false;
      bool isEmpty = true;

      for (final result in results) {
        if (result.recordResult.state == OtoCoreReadState.error) {
          errorMessage = result.recordResult.message;
          break;
        }
        if (result.statusResult.state == OtoCoreReadState.error) {
          errorMessage = result.statusResult.message;
          break;
        }
        if (result.recordResult.state == OtoCoreReadState.loading ||
            result.statusResult.state == OtoCoreReadState.loading) {
          isLoading = true;
          break;
        }
        if (result.recordResult.state == OtoCoreReadState.data) {
          isEmpty = false;
          final record = result.recordResult.data!;
          final status = result.statusResult.state == OtoCoreReadState.data
              ? result.statusResult.data
              : null;
          viewModels.add(
            OtoRunnerViewModel(
              runnerID: record.runnerID,
              alias: record.alias,
              status: status?.status ?? record.status,
              currentJobID: status?.currentJobID ?? '',
              currentExecutionID: status?.currentExecutionID ?? '',
              message: status?.message ?? record.failureReason,
            ),
          );
        }
      }

      if (errorMessage != null) {
        return OtoSurfaceSnapshot.error(errorMessage);
      } else if (isLoading) {
        return const OtoSurfaceSnapshot.loading();
      } else if (isEmpty) {
        return const OtoSurfaceSnapshot.empty();
      } else {
        return OtoSurfaceSnapshot.data(viewModels);
      }
    } on Exception catch (e) {
      return OtoSurfaceSnapshot.error(e.toString());
    }
  }

  Future<OtoSurfaceSnapshot<List<OtoJobViewModel>>> _fetchJobs(
    OtoCoreReadClient readClient,
  ) async {
    if (widget.jobIDs.isEmpty && _createdJobIDs.isEmpty) {
      return const OtoSurfaceSnapshot.empty();
    }

    final allJobIDs = <String>{
      ...widget.jobIDs,
      ..._createdJobIDs,
    };

    try {
      final results = await Future.wait(
        allJobIDs.map((id) => readClient.fetchJob(widget.config, id)),
      );

      final viewModels = <OtoJobViewModel>[];
      String? errorMessage;
      bool isLoading = false;
      bool isEmpty = true;

      for (final result in results) {
        if (result.state == OtoCoreReadState.error) {
          errorMessage = result.message;
          break;
        }
        if (result.state == OtoCoreReadState.loading) {
          isLoading = true;
          break;
        }
        if (result.state == OtoCoreReadState.data) {
          isEmpty = false;
          final record = result.data!;
          viewModels.add(
            OtoJobViewModel(
              jobID: record.id,
              name: record.name,
              state: record.state,
              executionID: record.executionID,
              createdAt: record.createdAt?.toIso8601String() ?? '',
              updatedAt: record.updatedAt?.toIso8601String() ?? '',
            ),
          );
        }
      }

      if (errorMessage != null) {
        return OtoSurfaceSnapshot.error(errorMessage);
      } else if (isLoading) {
        return const OtoSurfaceSnapshot.loading();
      } else if (isEmpty) {
        return const OtoSurfaceSnapshot.empty();
      } else {
        return OtoSurfaceSnapshot.data(viewModels);
      }
    } on Exception catch (e) {
      return OtoSurfaceSnapshot.error(e.toString());
    }
  }

  Future<OtoSurfaceSnapshot<List<OtoExecutionViewModel>>> _fetchExecutions(
    OtoCoreReadClient readClient,
  ) async {
    if (widget.executionIDs.isEmpty) {
      return const OtoSurfaceSnapshot.empty();
    }

    try {
      // Build executionID -> runnerID map from runner status.
      Map<String, String> runnerIDByExecution = <String, String>{};
      if (widget.runnerIDs.isNotEmpty) {
        final runnerStatusResults = await Future.wait(
          widget.runnerIDs.map((rid) => readClient.fetchRunnerStatus(widget.config, rid)),
        );
        for (final statusResult in runnerStatusResults) {
          if (statusResult.state == OtoCoreReadState.data && statusResult.data != null) {
            final status = statusResult.data!;
            if (status.currentExecutionID.isNotEmpty) {
              runnerIDByExecution[status.currentExecutionID] = status.runnerID;
            }
          }
        }
      }

      final results = await Future.wait(
        widget.executionIDs.map((id) => readClient.fetchExecution(widget.config, id)),
      );

      final viewModels = <OtoExecutionViewModel>[];
      String? errorMessage;
      bool isLoading = false;
      bool isEmpty = true;

      for (final result in results) {
        if (result.state == OtoCoreReadState.error) {
          errorMessage = result.message;
          break;
        }
        if (result.state == OtoCoreReadState.loading) {
          isLoading = true;
          break;
        }
        if (result.state == OtoCoreReadState.data) {
          isEmpty = false;
          final record = result.data!;
          viewModels.add(
            OtoExecutionViewModel(
              executionID: record.id,
              jobID: record.jobID,
              state: record.state,
              runnerID: runnerIDByExecution[record.id] ?? '',
              createdAt: record.createdAt?.toIso8601String() ?? '',
              updatedAt: record.updatedAt?.toIso8601String() ?? '',
            ),
          );
        }
      }

      if (errorMessage != null) {
        return OtoSurfaceSnapshot.error(errorMessage);
      } else if (isLoading) {
        return const OtoSurfaceSnapshot.loading();
      } else if (isEmpty) {
        return const OtoSurfaceSnapshot.empty();
      } else {
        return OtoSurfaceSnapshot.data(viewModels);
      }
    } on Exception catch (e) {
      return OtoSurfaceSnapshot.error(e.toString());
    }
  }

  Future<OtoSurfaceSnapshot<List<OtoArtifactViewModel>>> _fetchArtifacts(
    OtoCoreReadClient readClient,
  ) async {
    if (widget.executionIDs.isEmpty) {
      return const OtoSurfaceSnapshot.empty();
    }

    try {
      final results = await Future.wait(
        widget.executionIDs.map((id) => readClient.fetchArtifacts(widget.config, id)),
      );

      final viewModels = <OtoArtifactViewModel>[];
      String? errorMessage;
      bool isLoading = false;
      bool isEmpty = true;

      for (final result in results) {
        if (result.state == OtoCoreReadState.error) {
          errorMessage = result.message;
          break;
        }
        if (result.state == OtoCoreReadState.loading) {
          isLoading = true;
          break;
        }
        if (result.state == OtoCoreReadState.data || result.state == OtoCoreReadState.empty) {
          if (result.state == OtoCoreReadState.data) {
            isEmpty = false;
            final records = result.data ?? [];
            for (final record in records) {
              viewModels.add(
                OtoArtifactViewModel(
                  name: record.name,
                  path: record.path,
                ),
              );
            }
          }
        }
      }

      if (errorMessage != null) {
        return OtoSurfaceSnapshot.error(errorMessage);
      } else if (isLoading) {
        return const OtoSurfaceSnapshot.loading();
      } else if (isEmpty && viewModels.isEmpty) {
        return const OtoSurfaceSnapshot.empty();
      } else {
        return OtoSurfaceSnapshot.data(viewModels);
      }
    } on Exception catch (e) {
      return OtoSurfaceSnapshot.error(e.toString());
    }
  }

  Future<void> _loadExecutionDetail(String? execID) async {
    if (execID == null) {
      setState(() {
        _expandedExecutionID = null;
        _logsSnapshot = null;
        _expandedArtifactsSnapshot = null;
      });
      return;
    }

    setState(() {
      _expandedExecutionID = execID;
      _logsSnapshot = const OtoSurfaceSnapshot.loading();
      _expandedArtifactsSnapshot = const OtoSurfaceSnapshot.loading();
    });

    final readClient = widget.readClient ?? _defaultReadClient;

    try {
      final Future<OtoCoreReadResult<List<OtoExecutionLogEntry>>> logsFuture =
          readClient.fetchLogs(widget.config, execID);

      final Future<OtoCoreReadResult<List<OtoArtifactRecord>>> artifactsFuture =
          readClient.fetchArtifacts(widget.config, execID);

      final results = await Future.wait([logsFuture, artifactsFuture]);
      final logsResult = results[0] as OtoCoreReadResult<List<OtoExecutionLogEntry>>;
      final artifactsResult = results[1] as OtoCoreReadResult<List<OtoArtifactRecord>>;

      if (!mounted || _expandedExecutionID != execID) {
        return;
      }

      setState(() {
        if (logsResult.state == OtoCoreReadState.error) {
          _logsSnapshot = OtoSurfaceSnapshot.error(logsResult.message);
        } else if (logsResult.state == OtoCoreReadState.loading) {
          _logsSnapshot = const OtoSurfaceSnapshot.loading();
        } else if (logsResult.state == OtoCoreReadState.empty || logsResult.data == null) {
          _logsSnapshot = const OtoSurfaceSnapshot.empty();
        } else {
          _logsSnapshot = OtoSurfaceSnapshot.data(
            logsResult.data!
                .map((e) => OtoLogEntryViewModel(
                      timestamp: e.timestamp?.toIso8601String() ?? '',
                      line: e.line,
                    ))
                .toList(),
          );
        }

        if (artifactsResult.state == OtoCoreReadState.error) {
          _expandedArtifactsSnapshot =
              OtoSurfaceSnapshot.error(artifactsResult.message);
        } else if (artifactsResult.state == OtoCoreReadState.loading) {
          _expandedArtifactsSnapshot = const OtoSurfaceSnapshot.loading();
        } else if (artifactsResult.state == OtoCoreReadState.empty || artifactsResult.data == null) {
          _expandedArtifactsSnapshot = const OtoSurfaceSnapshot.empty();
        } else {
          _expandedArtifactsSnapshot = OtoSurfaceSnapshot.data(
            artifactsResult.data!
                .map((e) => OtoArtifactViewModel(
                      name: e.name,
                      path: e.path,
                    ))
                .toList(),
          );
        }
      });
    } on Exception catch (e) {
      if (mounted && _expandedExecutionID == execID) {
        setState(() {
          _logsSnapshot = OtoSurfaceSnapshot.error(e.toString());
          _expandedArtifactsSnapshot = OtoSurfaceSnapshot.error(e.toString());
        });
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    const themeAdapter = OtoConsoleThemeAdapter();
    return MaterialApp(
      title: 'OTO Client',
      debugShowCheckedModeBanner: false,
      theme: ThemeData.dark(useMaterial3: true).copyWith(
        colorScheme: ColorScheme.fromSeed(
          seedColor: themeAdapter.primaryColor,
          brightness: Brightness.dark,
          primary: themeAdapter.primaryColor,
          secondary: themeAdapter.accentColor,
        ),
        scaffoldBackgroundColor: themeAdapter.backgroundColor,
      ),
      home: OtoConsoleShell(
        config: widget.config,
        capabilities: widget.capabilities,
        themeAdapter: themeAdapter,
        initialSection: _activeSection,
        onNavigate: (section) {
          setState(() {
            _activeSection = section;
          });
        },
        overview: OtoConsoleOverview(
          config: widget.config,
          connection: _connection,
          onRefresh: _refreshCoreStatus,
          themeAdapter: themeAdapter,
        ),
        runners: OtoRunnersSurface(
          snapshot: _runnersSnapshot,
          actionStates: _runnerActionStates,
          onSelfUpdateRunner: _submitRunnerSelfUpdate,
          themeAdapter: themeAdapter,
        ),
        pipelines: OtoJobsSurface(
          snapshot: _jobsSnapshot,
          themeAdapter: themeAdapter,
          jobCreateState: _jobCreateState,
          onCreateJob: _submitJobCreate,
          onExecutionTap: (execID) {
            _loadExecutionDetail(execID);
            setState(() {
              _activeSection = OtoConsoleSection.executions;
            });
          },
        ),
        executions: OtoExecutionsSurface(
          snapshot: _executionsSnapshot,
          expandedExecutionID: _expandedExecutionID,
          actionStates: _executionActionStates,
          logsSnapshot: _logsSnapshot,
          artifactsSnapshot: _expandedArtifactsSnapshot,
          onExpandExecution: _loadExecutionDetail,
          onViewArtifactsSection: () {
            setState(() {
              _activeSection = OtoConsoleSection.artifacts;
            });
          },
          onCancelExecution: _submitCancelExecution,
          onReportExecution: _submitReportExecution,
          onAppendLog: _submitAppendExecutionLog,
          onAppendArtifact: _submitAppendExecutionArtifact,
          themeAdapter: themeAdapter,
        ),
        artifacts: OtoArtifactsSurface(
          snapshot: _artifactsSnapshot,
          themeAdapter: themeAdapter,
        ),
      ),
    );
  }
}

class _RunnerFetchResult {
  final String id;
  final OtoCoreReadResult<OtoRunnerRecord> recordResult;
  final OtoCoreReadResult<OtoRunnerStatus> statusResult;

  _RunnerFetchResult(this.id, this.recordResult, this.statusResult);
}
