import 'dart:async';

import 'package:flutter/material.dart';

import '../../models/jenkins_build.dart';
import '../../services/artifact_staging_service.dart';
import '../../services/jenkins_client.dart';

typedef JenkinsJobLoader = Future<List<JenkinsJob>> Function();

typedef JenkinsBuildLoader =
    Future<List<JenkinsBuild>> Function(JenkinsJob job);

typedef InstallRequestHandler =
    void Function(
      JenkinsBuild build,
      BuildArtifact artifact,
      StagedApk? stagedApk,
    );

/// Returns a [DownloadTask] that emits [DownloadProgressEvent]s carrying both
/// chunk bytes and cumulative progress. The task can be cancelled mid-stream.
typedef ArtifactDownloader =
    DownloadTask Function(JenkinsBuild build, BuildArtifact artifact);

typedef ArtifactStager =
    Future<StagedApk> Function({
      required JenkinsBuild build,
      required BuildArtifact artifact,
      required Stream<List<int>> byteStream,
    });

typedef ArtifactCleaner = Future<void> Function(String path);

enum JenkinsConnectionStatus { missing, reauthRequired, networkFailure, serverFailure }

enum _JobLoadState { loading, loaded, empty, forbidden, failure }

enum _BuildLoadState { loading, loaded, empty, forbidden, failure }

class BuildsPage extends StatefulWidget {
  const BuildsPage({
    super.key,
    this.jobLoader,
    this.buildLoader,
    this.onInstallRequested,
    this.artifactDownloader,
    this.artifactStager,
    this.artifactCleaner,
    this.connectionStatus,
    this.onRetryConnection,
  });

  final JenkinsJobLoader? jobLoader;
  final JenkinsBuildLoader? buildLoader;
  final InstallRequestHandler? onInstallRequested;
  final ArtifactDownloader? artifactDownloader;
  final ArtifactStager? artifactStager;
  final ArtifactCleaner? artifactCleaner;
  final JenkinsConnectionStatus? connectionStatus;
  final VoidCallback? onRetryConnection;

  @override
  State<BuildsPage> createState() => _BuildsPageState();
}

class _BuildsPageState extends State<BuildsPage> {
  _JobLoadState _state = _JobLoadState.loaded;
  List<JenkinsJob> _jobs = const [];
  List<JenkinsJob> _filtered = const [];
  JenkinsJob? _selectedJob;

  @override
  void initState() {
    super.initState();
    if (widget.jobLoader != null) {
      _load();
    }
  }

  @override
  void didUpdateWidget(covariant BuildsPage oldWidget) {
    super.didUpdateWidget(oldWidget);
    final hadLoader = oldWidget.jobLoader != null;
    final hasLoader = widget.jobLoader != null;

    if (!hadLoader && hasLoader) {
      _load();
      return;
    }

    if (hadLoader && !hasLoader) {
      setState(() {
        _state = _JobLoadState.loaded;
        _jobs = const [];
        _filtered = const [];
        _selectedJob = null;
      });
    }
  }

  Future<void> _load() async {
    setState(() {
      _state = _JobLoadState.loading;
      _jobs = const [];
      _filtered = const [];
      _selectedJob = null;
    });
    try {
      final jobs = await widget.jobLoader!();
      if (!mounted) return;
      setState(() {
        _jobs = jobs;
        _filtered = jobs;
        _state = jobs.isEmpty ? _JobLoadState.empty : _JobLoadState.loaded;
      });
    } on JenkinsClientException catch (e) {
      if (!mounted) return;
      setState(() {
        _state = (e.statusCode == 401 || e.statusCode == 403)
            ? _JobLoadState.forbidden
            : _JobLoadState.failure;
      });
    } catch (_) {
      if (!mounted) return;
      setState(() => _state = _JobLoadState.failure);
    }
  }

  void _setQuery(String query) {
    setState(() {
      _filtered = query.isEmpty
          ? _jobs
          : _jobs.where((j) {
              final q = query.toLowerCase();
              return j.name.toLowerCase().contains(q) ||
                  (j.fullName?.toLowerCase().contains(q) ?? false) ||
                  j.url.toString().toLowerCase().contains(q);
            }).toList();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.fromLTRB(28, 0, 28, 28),
      child: _selectedJob != null
          ? _JobSelected(
              key: ValueKey(_selectedJob!.url),
              job: _selectedJob!,
              buildLoader: widget.buildLoader,
              onInstallRequested: widget.onInstallRequested,
              artifactDownloader: widget.artifactDownloader,
              artifactStager: widget.artifactStager,
              artifactCleaner: widget.artifactCleaner,
              onBack: () => setState(() => _selectedJob = null),
            )
          : _buildJobSelector(context),
    );
  }

  Widget _buildJobSelector(BuildContext context) {
    if (widget.jobLoader == null) {
      final retry = widget.onRetryConnection;
      return switch (widget.connectionStatus) {
        JenkinsConnectionStatus.reauthRequired => const _JobStatus(
          icon: Icons.lock_reset,
          label: '재로그인 필요',
          description:
              'token이 폐기되었거나 접근 권한이 없습니다. 설정에서 Web Login을 다시 실행하세요.',
        ),
        JenkinsConnectionStatus.networkFailure => _JobStatus(
          icon: Icons.wifi_off,
          label: 'Jenkins 연결 실패',
          description: '네트워크에 연결할 수 없습니다.',
          onAction: retry,
          actionLabel: '재시도',
        ),
        JenkinsConnectionStatus.serverFailure => _JobStatus(
          icon: Icons.dns_outlined,
          label: 'Jenkins 응답 오류',
          description: 'Jenkins 서버에서 오류가 발생했습니다.',
          onAction: retry,
          actionLabel: '재시도',
        ),
        JenkinsConnectionStatus.missing || null => const _JobStatus(
          icon: Icons.login,
          label: 'Jenkins 연결 필요',
          description: '설정에서 Jenkins URL과 인증 정보를 입력하세요.',
        ),
      };
    }
    return switch (_state) {
      _JobLoadState.loading => const Center(child: CircularProgressIndicator()),
      _JobLoadState.empty => const _JobStatus(
        icon: Icons.inbox,
        label: '접근 가능한 job 없음',
        description: 'Jenkins 계정에 접근 가능한 job이 없습니다.',
      ),
      _JobLoadState.forbidden => const _JobStatus(
        icon: Icons.lock,
        label: '권한 없음',
        description: 'Jenkins 인증이 필요하거나 접근 권한이 없습니다.',
      ),
      _JobLoadState.failure => const _JobStatus(
        icon: Icons.wifi_off,
        label: 'Jenkins 조회 실패',
        description: '네트워크 오류 또는 Jenkins 서버에 연결할 수 없습니다.',
      ),
      _JobLoadState.loaded => _buildJobList(context),
    };
  }

  Widget _buildJobList(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        TextField(
          key: const ValueKey('job-search-field'),
          decoration: const InputDecoration(
            labelText: 'Job 검색',
            prefixIcon: Icon(Icons.search),
          ),
          onChanged: _setQuery,
        ),
        const SizedBox(height: 12),
        Expanded(
          child: _filtered.isEmpty
              ? Center(
                  child: Text(
                    '검색 결과 없음',
                    style: TextStyle(
                      color: Theme.of(context).colorScheme.onSurfaceVariant,
                    ),
                  ),
                )
              : ListView.separated(
                  itemCount: _filtered.length,
                  separatorBuilder: (_, _) => const SizedBox(height: 8),
                  itemBuilder: (context, index) => _JobRow(
                    job: _filtered[index],
                    onTap: () =>
                        setState(() => _selectedJob = _filtered[index]),
                  ),
                ),
        ),
      ],
    );
  }
}

class _JobSelected extends StatefulWidget {
  const _JobSelected({
    super.key,
    required this.job,
    required this.onBack,
    this.buildLoader,
    this.onInstallRequested,
    this.artifactDownloader,
    this.artifactStager,
    this.artifactCleaner,
  });

  final JenkinsJob job;
  final VoidCallback onBack;
  final JenkinsBuildLoader? buildLoader;
  final InstallRequestHandler? onInstallRequested;
  final ArtifactDownloader? artifactDownloader;
  final ArtifactStager? artifactStager;
  final ArtifactCleaner? artifactCleaner;

  @override
  State<_JobSelected> createState() => _JobSelectedState();
}

class _JobSelectedState extends State<_JobSelected> {
  late _BuildLoadState _buildState;
  List<JenkinsBuild> _builds = const [];
  List<JenkinsBuild> _filtered = const [];
  JenkinsBuild? _selectedBuild;

  @override
  void initState() {
    super.initState();
    if (widget.buildLoader != null) {
      _buildState = _BuildLoadState.loading;
      _loadBuilds();
    } else {
      _buildState = _BuildLoadState.empty;
    }
  }

  Future<void> _loadBuilds() async {
    try {
      final builds = await widget.buildLoader!(widget.job);
      if (!mounted) return;
      setState(() {
        _builds = builds;
        _filtered = builds;
        _selectedBuild = null;
        _buildState = builds.isEmpty
            ? _BuildLoadState.empty
            : _BuildLoadState.loaded;
      });
    } on JenkinsClientException catch (e) {
      if (!mounted) return;
      setState(() {
        _buildState = (e.statusCode == 401 || e.statusCode == 403)
            ? _BuildLoadState.forbidden
            : _BuildLoadState.failure;
      });
    } catch (_) {
      if (!mounted) return;
      setState(() => _buildState = _BuildLoadState.failure);
    }
  }

  void _setBuildQuery(String query) {
    setState(() {
      if (query.isEmpty) {
        _filtered = _builds;
      } else {
        final q = query.toLowerCase();
        _filtered = _builds.where((b) {
          return (b.requestedBy?.toLowerCase().contains(q) ?? false) ||
              b.number.toString().contains(q) ||
              (b.branch?.toLowerCase().contains(q) ?? false) ||
              (b.flavor?.toLowerCase().contains(q) ?? false) ||
              b.apkArtifacts.any(
                (a) =>
                    a.fileName.toLowerCase().contains(q) ||
                    a.relativePath.toLowerCase().contains(q),
              );
        }).toList();
      }

      if (_selectedBuild != null && !_filtered.contains(_selectedBuild)) {
        _selectedBuild = null;
      }
    });
  }

  void _selectBuild(JenkinsBuild build) {
    setState(() {
      _selectedBuild = identical(_selectedBuild, build) ? null : build;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Row(
          children: [
            IconButton(
              icon: const Icon(Icons.arrow_back),
              tooltip: '목록으로',
              onPressed: widget.onBack,
            ),
            const SizedBox(width: 8),
            Expanded(
              child: Text(
                widget.job.fullName ?? widget.job.name,
                style: Theme.of(context).textTheme.titleMedium,
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
              ),
            ),
          ],
        ),
        const SizedBox(height: 12),
        if (_buildState == _BuildLoadState.loaded) ...[
          TextField(
            key: const ValueKey('build-search-field'),
            decoration: const InputDecoration(
              labelText: 'Build 검색',
              prefixIcon: Icon(Icons.search),
            ),
            onChanged: _setBuildQuery,
          ),
          const SizedBox(height: 12),
        ],
        Expanded(child: _buildBody(context)),
      ],
    );
  }

  Widget _buildBody(BuildContext context) {
    return switch (_buildState) {
      _BuildLoadState.loading => const Center(
        child: CircularProgressIndicator(),
      ),
      _BuildLoadState.forbidden => const _BuildStatus(
        icon: Icons.lock,
        label: 'build 조회 권한 없음',
        description: 'Jenkins 인증이 필요하거나 이 job에 대한 접근 권한이 없습니다.',
      ),
      _BuildLoadState.failure => const _BuildStatus(
        icon: Icons.wifi_off,
        label: 'build 조회 실패',
        description: '네트워크 오류 또는 Jenkins 서버에 연결할 수 없습니다.',
      ),
      _BuildLoadState.empty => const _BuildStatus(
        icon: Icons.inbox,
        label: 'APK artifact 없음',
        description: '이 job에 접근 가능한 build가 없습니다.',
      ),
      _BuildLoadState.loaded =>
        _filtered.isEmpty
            ? Center(
                child: Text(
                  '검색 결과 없음',
                  style: TextStyle(
                    color: Theme.of(context).colorScheme.onSurfaceVariant,
                  ),
                ),
              )
            : Column(
                children: [
                  Expanded(
                    child: ListView.separated(
                      itemCount: _filtered.length,
                      separatorBuilder: (_, _) => const SizedBox(height: 8),
                      itemBuilder: (context, index) {
                        final item = _filtered[index];
                        return _BuildRow(
                          item: item,
                          selected: identical(_selectedBuild, item),
                          onTap: () => _selectBuild(item),
                        );
                      },
                    ),
                  ),
                  if (_selectedBuild case final selectedBuild?)
                    if (selectedBuild.apkArtifacts case [final artifact, ...])
                      Padding(
                        padding: const EdgeInsets.only(top: 12),
                        child: _InstallCta(
                          key: ValueKey('install-cta-${selectedBuild.number}'),
                          item: selectedBuild,
                          artifact: artifact,
                          onInstallRequested: widget.onInstallRequested,
                          artifactDownloader: widget.artifactDownloader,
                          artifactStager: widget.artifactStager,
                          artifactCleaner: widget.artifactCleaner,
                        ),
                      ),
                ],
              ),
    };
  }
}

class _BuildRow extends StatelessWidget {
  const _BuildRow({
    required this.item,
    required this.selected,
    required this.onTap,
  });

  final JenkinsBuild item;
  final bool selected;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;
    final apkNames = item.apkArtifacts.map((a) => a.fileName).join(', ');

    final summaryParts = [
      '#${item.number}',
      if (item.branch != null) item.branch!,
      if (item.flavor != null) item.flavor!,
      if (item.startedAt != null) _formatDate(item.startedAt!),
      if (apkNames.isNotEmpty) apkNames,
    ];

    return Material(
      key: ValueKey('build-row-${item.number}'),
      color: selected
          ? colorScheme.primaryContainer.withValues(alpha: 0.36)
          : Colors.white,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(8),
        side: BorderSide(
          color: selected ? colorScheme.primary : colorScheme.outlineVariant,
          width: selected ? 1.5 : 1,
        ),
      ),
      child: InkWell(
        borderRadius: BorderRadius.circular(8),
        onTap: onTap,
        child: Padding(
          padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              _StateBadge(item: item),
              const SizedBox(width: 12),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      item.requestedBy ?? '알 수 없는 사용자',
                      style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                        fontWeight: FontWeight.w600,
                      ),
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                    if (summaryParts.isNotEmpty)
                      Text(
                        summaryParts.join(' · '),
                        style: TextStyle(
                          color: colorScheme.onSurfaceVariant,
                          fontSize: 12,
                        ),
                        maxLines: 2,
                        overflow: TextOverflow.ellipsis,
                      ),
                  ],
                ),
              ),
              const SizedBox(width: 8),
              Icon(
                selected ? Icons.radio_button_checked : Icons.circle_outlined,
                color: selected
                    ? colorScheme.primary
                    : colorScheme.onSurfaceVariant,
                size: 20,
              ),
            ],
          ),
        ),
      ),
    );
  }

  String _formatDate(DateTime dt) {
    final mo = dt.month.toString().padLeft(2, '0');
    final d = dt.day.toString().padLeft(2, '0');
    final h = dt.hour.toString().padLeft(2, '0');
    final mi = dt.minute.toString().padLeft(2, '0');
    return '${dt.year}-$mo-$d $h:$mi';
  }
}

enum _DownloadState { idle, downloading, verified, failed, cancelled }

class _ArtifactDownloadCancelledException implements Exception {
  const _ArtifactDownloadCancelledException();

  @override
  String toString() => 'Artifact download cancelled';
}

class _InstallCta extends StatefulWidget {
  const _InstallCta({
    super.key,
    required this.item,
    required this.artifact,
    this.onInstallRequested,
    this.artifactDownloader,
    this.artifactStager,
    this.artifactCleaner,
  });

  final JenkinsBuild item;
  final BuildArtifact artifact;
  final InstallRequestHandler? onInstallRequested;
  final ArtifactDownloader? artifactDownloader;
  final ArtifactStager? artifactStager;
  final ArtifactCleaner? artifactCleaner;

  @override
  State<_InstallCta> createState() => _InstallCtaState();
}

class _InstallCtaState extends State<_InstallCta> {
  _DownloadState _dlState = _DownloadState.idle;
  int _receivedBytes = 0;
  int? _totalBytes;
  StagedApk? _stagedApk;
  String? _errorMessage;
  DownloadTask? _task;
  StreamSubscription<DownloadProgressEvent>? _eventSub;
  StreamController<List<int>>? _byteController;
  bool _handoffRequested = false;

  @override
  void dispose() {
    _cancelDownload();
    if (!_handoffRequested) {
      _cleanupStagedApk();
    }
    super.dispose();
  }

  Future<void> _startDownload() async {
    final downloader = widget.artifactDownloader;
    final stager = widget.artifactStager;
    if (downloader == null || stager == null) return;

    _cleanupStagedApk();
    _handoffRequested = false;

    final task = downloader(widget.item, widget.artifact);
    final byteController = StreamController<List<int>>();

    setState(() {
      _dlState = _DownloadState.downloading;
      _receivedBytes = 0;
      _totalBytes = null;
      _stagedApk = null;
      _errorMessage = null;
      _task = task;
      _byteController = byteController;
    });

    // Forward chunk bytes to stager; update progress UI from event metadata.
    _eventSub = task.events.listen(
      (event) {
        if (!mounted || byteController.isClosed) return;
        setState(() {
          _receivedBytes = event.receivedBytes;
          _totalBytes = event.totalBytes;
        });
        if (event.chunkBytes.isNotEmpty) {
          byteController.add(event.chunkBytes);
        }
        if (event.isComplete && !byteController.isClosed) {
          byteController.close();
        }
      },
      onError: (Object e) {
        if (!byteController.isClosed) {
          byteController.addError(e);
          byteController.close();
        }
      },
      onDone: () {
        if (!byteController.isClosed) {
          byteController.close();
        }
      },
      cancelOnError: true,
    );

    try {
      final staged = await stager(
        build: widget.item,
        artifact: widget.artifact,
        byteStream: byteController.stream,
      );
      if (!mounted) return;
      setState(() {
        _dlState = _DownloadState.verified;
        _stagedApk = staged;
      });
    } catch (e) {
      if (!mounted) return;
      if (_dlState == _DownloadState.cancelled) return;
      setState(() {
        _dlState = _DownloadState.failed;
        _errorMessage = e.toString();
      });
    } finally {
      await _eventSub?.cancel();
      _eventSub = null;
      _task = null;
      _byteController = null;
    }
  }

  void _cancelDownload() {
    _task?.cancel();
    _task = null;
    _eventSub?.cancel();
    _eventSub = null;
    final bc = _byteController;
    if (bc != null && !bc.isClosed) {
      bc.addError(const _ArtifactDownloadCancelledException());
      bc.close();
    }
    _byteController = null;
  }

  void _cleanupStagedApk() {
    final staged = _stagedApk;
    _stagedApk = null;
    final cleaner = widget.artifactCleaner;
    if (staged != null && cleaner != null) {
      unawaited(_cleanStagedPath(cleaner, staged.path));
    }
  }

  Future<void> _cleanStagedPath(ArtifactCleaner cleaner, String path) async {
    try {
      await cleaner(path);
    } catch (_) {
      // Cleanup is best-effort; the install flow should not fail during dispose.
    }
  }

  void _cancel() {
    _cancelDownload();
    if (!mounted) return;
    setState(() => _dlState = _DownloadState.cancelled);
  }

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;

    return Material(
      key: const ValueKey('install-cta'),
      color: colorScheme.primaryContainer.withValues(alpha: 0.42),
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(8),
        side: BorderSide(color: colorScheme.primary.withValues(alpha: 0.34)),
      ),
      child: Padding(
        padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
        child: switch (_dlState) {
          _DownloadState.idle ||
          _DownloadState.cancelled => _buildIdle(context, colorScheme),
          _DownloadState.downloading => _buildDownloading(context, colorScheme),
          _DownloadState.verified => _buildVerified(context, colorScheme),
          _DownloadState.failed => _buildFailed(context, colorScheme),
        },
      ),
    );
  }

  Widget _buildIdle(BuildContext context, ColorScheme colorScheme) {
    final hasDownloader =
        widget.artifactDownloader != null && widget.artifactStager != null;
    return Row(
      children: [
        Icon(Icons.install_mobile, color: colorScheme.primary),
        const SizedBox(width: 12),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                _dlState == _DownloadState.cancelled ? '다운로드 취소됨' : '설치 준비',
                style: Theme.of(
                  context,
                ).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w700),
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
              ),
              Text(
                '#${widget.item.number} · ${widget.artifact.fileName}',
                style: TextStyle(
                  color: colorScheme.onSurfaceVariant,
                  fontSize: 12,
                ),
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
              ),
            ],
          ),
        ),
        const SizedBox(width: 12),
        if (hasDownloader)
          ElevatedButton.icon(
            key: const ValueKey('install-cta-button'),
            onPressed: _startDownload,
            icon: const Icon(Icons.download),
            label: const Text('다운로드'),
          )
        else
          ElevatedButton.icon(
            key: const ValueKey('install-cta-button'),
            onPressed: widget.onInstallRequested == null
                ? null
                : () => widget.onInstallRequested!(
                    widget.item,
                    widget.artifact,
                    null,
                  ),
            icon: const Icon(Icons.install_mobile),
            label: const Text('설치'),
          ),
      ],
    );
  }

  Widget _buildDownloading(BuildContext context, ColorScheme colorScheme) {
    final total = _totalBytes;
    final progress = (total != null && total > 0)
        ? _receivedBytes / total
        : null;
    final label = total != null
        ? '${_fmt(_receivedBytes)} / ${_fmt(total)}'
        : '${_fmt(_receivedBytes)} 받는 중…';

    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Row(
          children: [
            Icon(Icons.download, color: colorScheme.primary),
            const SizedBox(width: 12),
            Expanded(
              child: Text(
                key: const ValueKey('download-progress-label'),
                label,
                style: TextStyle(
                  color: colorScheme.onSurfaceVariant,
                  fontSize: 12,
                ),
              ),
            ),
            TextButton(
              key: const ValueKey('download-cancel-button'),
              onPressed: _cancel,
              child: const Text('취소'),
            ),
          ],
        ),
        const SizedBox(height: 6),
        LinearProgressIndicator(
          key: const ValueKey('download-progress-bar'),
          value: progress,
        ),
      ],
    );
  }

  Widget _buildVerified(BuildContext context, ColorScheme colorScheme) {
    return Row(
      children: [
        Icon(Icons.check_circle, color: colorScheme.primary),
        const SizedBox(width: 12),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                key: const ValueKey('verified-label'),
                '다운로드 완료',
                style: Theme.of(
                  context,
                ).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w700),
              ),
              Text(
                _stagedApk?.path ??
                    '#${widget.item.number} · ${widget.artifact.fileName}',
                style: TextStyle(
                  color: colorScheme.onSurfaceVariant,
                  fontSize: 12,
                ),
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
              ),
            ],
          ),
        ),
        const SizedBox(width: 12),
        ElevatedButton.icon(
          key: const ValueKey('install-cta-button'),
          onPressed:
              widget.onInstallRequested == null ||
                  _stagedApk == null ||
                  _handoffRequested
              ? null
              : () {
                  setState(() => _handoffRequested = true);
                  widget.onInstallRequested!(
                    widget.item,
                    widget.artifact,
                    _stagedApk,
                  );
                },
          icon: const Icon(Icons.install_mobile),
          label: const Text('설치'),
        ),
      ],
    );
  }

  Widget _buildFailed(BuildContext context, ColorScheme colorScheme) {
    return Row(
      children: [
        Icon(Icons.error_outline, color: colorScheme.error),
        const SizedBox(width: 12),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                key: const ValueKey('download-error-label'),
                '다운로드 실패',
                style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                  color: colorScheme.error,
                  fontWeight: FontWeight.w700,
                ),
              ),
              if (_errorMessage != null)
                Text(
                  _errorMessage!,
                  style: TextStyle(color: colorScheme.error, fontSize: 11),
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
            ],
          ),
        ),
        const SizedBox(width: 12),
        TextButton(
          key: const ValueKey('install-cta-button'),
          onPressed: _startDownload,
          child: const Text('재시도'),
        ),
      ],
    );
  }

  String _fmt(int bytes) {
    if (bytes < 1024) return '$bytes B';
    if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
    return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
  }
}

class _StateBadge extends StatelessWidget {
  const _StateBadge({required this.item});

  final JenkinsBuild item;

  @override
  Widget build(BuildContext context) {
    final (label, color) = _labelAndColor();
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
      decoration: BoxDecoration(
        color: color.withValues(alpha: 0.12),
        borderRadius: BorderRadius.circular(4),
        border: Border.all(color: color.withValues(alpha: 0.4)),
      ),
      child: Text(
        label,
        style: TextStyle(
          fontSize: 11,
          fontWeight: FontWeight.w600,
          color: color,
        ),
      ),
    );
  }

  (String, Color) _labelAndColor() {
    if (item.isRunning) return ('실행 중', Colors.blue);
    return switch (item.result) {
      'SUCCESS' => ('성공', Colors.green),
      'FAILURE' => ('실패', Colors.red),
      'ABORTED' => ('중단됨', Colors.orange),
      _ => (item.result ?? '알 수 없음', Colors.grey),
    };
  }
}

class _BuildStatus extends StatelessWidget {
  const _BuildStatus({
    required this.icon,
    required this.label,
    required this.description,
  });

  final IconData icon;
  final String label;
  final String description;

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;

    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(icon, size: 48, color: colorScheme.onSurfaceVariant),
          const SizedBox(height: 12),
          Text(label, style: Theme.of(context).textTheme.titleMedium),
          const SizedBox(height: 4),
          Text(
            description,
            style: TextStyle(color: colorScheme.onSurfaceVariant),
            textAlign: TextAlign.center,
          ),
        ],
      ),
    );
  }
}

class _JobRow extends StatelessWidget {
  const _JobRow({required this.job, required this.onTap});

  final JenkinsJob job;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;

    return Material(
      color: Colors.white,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(8),
        side: BorderSide(color: colorScheme.outlineVariant),
      ),
      child: InkWell(
        borderRadius: BorderRadius.circular(8),
        onTap: onTap,
        child: Padding(
          padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
          child: Row(
            children: [
              Icon(
                job.isFolder ? Icons.folder_outlined : Icons.build_outlined,
                color: colorScheme.primary,
              ),
              const SizedBox(width: 12),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      job.name,
                      style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                        fontWeight: FontWeight.w600,
                      ),
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                    if (job.fullName != null && job.fullName != job.name)
                      Text(
                        job.fullName!,
                        style: TextStyle(
                          color: colorScheme.onSurfaceVariant,
                          fontSize: 12,
                        ),
                        maxLines: 1,
                        overflow: TextOverflow.ellipsis,
                      ),
                  ],
                ),
              ),
              Icon(Icons.chevron_right, color: colorScheme.onSurfaceVariant),
            ],
          ),
        ),
      ),
    );
  }
}

class _JobStatus extends StatelessWidget {
  const _JobStatus({
    required this.icon,
    required this.label,
    required this.description,
    this.onAction,
    this.actionLabel,
  });

  final IconData icon;
  final String label;
  final String description;
  final VoidCallback? onAction;
  final String? actionLabel;

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;

    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(icon, size: 48, color: colorScheme.onSurfaceVariant),
          const SizedBox(height: 12),
          Text(label, style: Theme.of(context).textTheme.titleMedium),
          const SizedBox(height: 4),
          Text(
            description,
            style: TextStyle(color: colorScheme.onSurfaceVariant),
            textAlign: TextAlign.center,
          ),
          if (onAction != null && actionLabel != null) ...[
            const SizedBox(height: 16),
            ElevatedButton(
              key: const ValueKey('job-status-action-button'),
              onPressed: onAction,
              child: Text(actionLabel!),
            ),
          ],
        ],
      ),
    );
  }
}
