// ChainLogViewer — modal sheet that renders a log file inline with
// line numbers, basic syntax colouring and a Copy-all button.
//
// Use [showFaiLogViewer] to open it. Two log shapes are supported
// out of the box:
//
//   - JSON-per-line (Studio's `~/.chain/logs/studio-errors.log`):
//     fields `ts`, `level`, `source`, `error` get distinct colours;
//     anything else renders neutral.
//   - tracing text (the daemon's `~/.chain/run/<channel>.log`):
//     `[INFO]`, `[WARN]`, `[ERROR]`, `[DEBUG]` substrings get the
//     matching tone from the ChainTheme palette.
//
// The viewer reads the *last* N lines (default 500) to keep
// rendering snappy on huge log files. A Refresh button re-reads
// the file in place. An "Open externally" button still hands the
// path to the OS so power users can pipe it through their tool
// of choice.

import 'dart:io';

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

import '../data/system_actions.dart';
import '../l10n/app_localizations.dart';
import '../theme/theme.dart';
import '../theme/tokens.dart';

/// Open [path] inside a modal log viewer.
///
/// [title] is shown in the AppBar (e.g. `"local.log"` or
/// `"studio-errors.log"`); falls back to the basename of [path].
Future<void> showFaiLogViewer(
  BuildContext context, {
  required String path,
  String? title,
  int tailLines = 500,
  bool fromTop = false,
  bool plain = false,
}) {
  return showDialog<void>(
    context: context,
    barrierDismissible: true,
    builder: (_) => ChainLogViewer(
      path: path,
      title: title ?? path.split(Platform.pathSeparator).last,
      tailLines: tailLines,
      fromTop: fromTop,
      plain: plain,
    ),
  );
}

/// Open a config / plain-text file (e.g. `config.yaml`) inside the
/// same modal viewer, read top-down with no log-level colouring.
/// A thin wrapper over [showFaiLogViewer] so call sites read by
/// intent.
Future<void> showFaiConfigViewer(
  BuildContext context, {
  required String path,
  String? title,
  int maxLines = 2000,
}) {
  return showFaiLogViewer(
    context,
    path: path,
    title: title,
    tailLines: maxLines,
    fromTop: true,
    plain: true,
  );
}

class ChainLogViewer extends StatefulWidget {
  final String path;
  final String title;
  final int tailLines;

  /// Read from the top of the file (config) vs the tail (logs).
  final bool fromTop;

  /// Skip log-level colouring and render every line neutral
  /// (for config / plain-text files).
  final bool plain;

  const ChainLogViewer({
    super.key,
    required this.path,
    required this.title,
    this.tailLines = 500,
    this.fromTop = false,
    this.plain = false,
  });

  @override
  State<ChainLogViewer> createState() => _FaiLogViewerState();
}

class _FaiLogViewerState extends State<ChainLogViewer> {
  List<String> _lines = const <String>[];
  bool _loading = true;
  bool _justCopied = false;

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

  Future<void> _reload() async {
    setState(() => _loading = true);
    final lines = await _readLines(
      widget.path,
      widget.tailLines,
      fromTop: widget.fromTop,
    );
    if (!mounted) return;
    setState(() {
      _lines = lines;
      _loading = false;
    });
  }

  Future<void> _copyAll() async {
    await Clipboard.setData(ClipboardData(text: _lines.join('\n')));
    if (!mounted) return;
    setState(() => _justCopied = true);
    Future.delayed(const Duration(seconds: 2), () {
      if (mounted) setState(() => _justCopied = false);
    });
  }

  Future<void> _openExternal() async {
    await SystemActions.openOrReveal(widget.path, isDirectory: false);
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    return Dialog(
      insetPadding: const EdgeInsets.symmetric(
        horizontal: ChainSpace.xl,
        vertical: ChainSpace.xl,
      ),
      child: ConstrainedBox(
        constraints: const BoxConstraints(maxWidth: 980, maxHeight: 720),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            _Header(
              title: widget.title,
              path: widget.path,
              lineCount: _lines.length,
              tailLimit: widget.tailLines,
              loading: _loading,
              justCopied: _justCopied,
              onCopy: _copyAll,
              onReload: _reload,
              onOpenExternal: _openExternal,
              onClose: () => Navigator.of(context).pop(),
            ),
            Divider(height: 1, color: theme.colorScheme.outlineVariant),
            Expanded(
              child: _loading
                  ? const Center(child: CircularProgressIndicator())
                  : _lines.isEmpty
                  ? Center(
                      child: Padding(
                        padding: const EdgeInsets.all(ChainSpace.xl),
                        child: Text(
                          l.logViewerEmpty,
                          style: theme.textTheme.bodySmall?.copyWith(
                            color: theme.colorScheme.onSurfaceVariant,
                          ),
                        ),
                      ),
                    )
                  : _LogBody(lines: _lines, plain: widget.plain),
            ),
          ],
        ),
      ),
    );
  }
}

class _Header extends StatelessWidget {
  final String title;
  final String path;
  final int lineCount;
  final int tailLimit;
  final bool loading;
  final bool justCopied;
  final VoidCallback onCopy;
  final VoidCallback onReload;
  final VoidCallback onOpenExternal;
  final VoidCallback onClose;

  const _Header({
    required this.title,
    required this.path,
    required this.lineCount,
    required this.tailLimit,
    required this.loading,
    required this.justCopied,
    required this.onCopy,
    required this.onReload,
    required this.onOpenExternal,
    required this.onClose,
  });

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    return Padding(
      padding: const EdgeInsets.fromLTRB(
        ChainSpace.lg,
        ChainSpace.md,
        ChainSpace.sm,
        ChainSpace.md,
      ),
      child: Row(
        children: [
          Icon(
            Icons.description_outlined,
            size: 18,
            color: theme.colorScheme.onSurfaceVariant,
          ),
          const SizedBox(width: ChainSpace.sm),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisSize: MainAxisSize.min,
              children: [
                Text(
                  l.logViewerTitle(title, tailLimit),
                  style: theme.textTheme.titleSmall?.copyWith(
                    fontWeight: FontWeight.w600,
                  ),
                  overflow: TextOverflow.ellipsis,
                ),
                const SizedBox(height: 2),
                Text(
                  '$path · ${l.logViewerLineCount(lineCount)}',
                  style: ChainTheme.mono(
                    size: 10,
                    color: theme.colorScheme.onSurfaceVariant,
                  ),
                  overflow: TextOverflow.ellipsis,
                ),
              ],
            ),
          ),
          IconButton(
            tooltip: justCopied ? l.buttonCopied : l.logViewerCopyAll,
            icon: Icon(
              justCopied ? Icons.check : Icons.content_copy,
              size: 16,
            ),
            onPressed: loading || lineCount == 0 ? null : onCopy,
          ),
          IconButton(
            tooltip: l.buttonRefresh,
            icon: const Icon(Icons.refresh, size: 16),
            onPressed: loading ? null : onReload,
          ),
          IconButton(
            tooltip: l.logViewerOpenExternal,
            icon: const Icon(Icons.open_in_new, size: 16),
            onPressed: onOpenExternal,
          ),
          IconButton(
            tooltip: l.buttonClose,
            icon: const Icon(Icons.close, size: 16),
            onPressed: onClose,
          ),
        ],
      ),
    );
  }
}

class _LogBody extends StatelessWidget {
  final List<String> lines;
  final bool plain;

  const _LogBody({required this.lines, this.plain = false});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final gutterWidth = 12.0 + 8.0 * lines.length.toString().length;
    return Scrollbar(
      child: ListView.builder(
        padding: const EdgeInsets.symmetric(vertical: ChainSpace.sm),
        itemCount: lines.length,
        itemBuilder: (context, i) => _LogLine(
          lineNumber: i + 1,
          text: lines[i],
          gutterWidth: gutterWidth,
          theme: theme,
          plain: plain,
        ),
      ),
    );
  }
}

class _LogLine extends StatelessWidget {
  final int lineNumber;
  final String text;
  final double gutterWidth;
  final ThemeData theme;
  final bool plain;

  const _LogLine({
    required this.lineNumber,
    required this.text,
    required this.gutterWidth,
    required this.theme,
    this.plain = false,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: ChainSpace.sm, vertical: 1),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(
            width: gutterWidth,
            child: Text(
              '$lineNumber',
              textAlign: TextAlign.right,
              style: ChainTheme.mono(
                size: 11,
                color: theme.colorScheme.onSurfaceVariant.withValues(
                  alpha: 0.6,
                ),
              ),
            ),
          ),
          const SizedBox(width: ChainSpace.sm),
          Expanded(
            child: SelectableText.rich(
              plain
                  ? TextSpan(
                      text: text,
                      style: TextStyle(color: theme.colorScheme.onSurface),
                    )
                  : _highlightLine(text, theme),
              style: ChainTheme.mono(
                size: 11,
                color: theme.colorScheme.onSurface,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

/// Lightweight tokeniser shared by both supported log shapes.
///
/// We don't reach for a full grammar here on purpose: log files
/// are short-lived, the operator just needs the eye to land on
/// the level/source quickly. Real syntax highlighting belongs in
/// the flow editor; logs only need three or four accent colours.
TextSpan _highlightLine(String line, ThemeData theme) {
  // JSON-per-line path (the Studio error log). Looks like:
  // {"ts":"…","level":"error","source":"flows.run","error":"…"}
  if (line.startsWith('{') && line.endsWith('}')) {
    return _highlightJsonLine(line, theme);
  }
  return _highlightTraceLine(line, theme);
}

TextSpan _highlightJsonLine(String line, ThemeData theme) {
  final spans = <TextSpan>[];
  final base = TextStyle(color: theme.colorScheme.onSurface);
  final keyStyle = TextStyle(
    color: theme.colorScheme.primary,
    fontWeight: FontWeight.w600,
  );
  final stringStyle = TextStyle(color: ChainColors.success);
  final errorStringStyle = TextStyle(color: theme.colorScheme.error);

  final keyRe = RegExp(r'"(ts|level|source|error|context)"\s*:');
  final stringRe = RegExp(r'"((?:\\.|[^"\\])*)"');

  int cursor = 0;
  bool nextStringIsError = false;
  while (cursor < line.length) {
    final keyMatch = keyRe.matchAsPrefix(line, cursor);
    if (keyMatch != null) {
      spans.add(TextSpan(text: keyMatch.group(0), style: keyStyle));
      nextStringIsError = keyMatch.group(1) == 'error';
      cursor = keyMatch.end;
      continue;
    }
    final stringMatch = stringRe.matchAsPrefix(line, cursor);
    if (stringMatch != null) {
      spans.add(
        TextSpan(
          text: stringMatch.group(0),
          style: nextStringIsError ? errorStringStyle : stringStyle,
        ),
      );
      cursor = stringMatch.end;
      nextStringIsError = false;
      continue;
    }
    spans.add(TextSpan(text: line[cursor], style: base));
    cursor++;
  }
  return TextSpan(children: spans);
}

TextSpan _highlightTraceLine(String line, ThemeData theme) {
  // Tracing line: prefix tag like [INFO] / [WARN] / [ERROR] /
  // [DEBUG] gets the tone colour, everything else stays neutral.
  final tagRe = RegExp(r'\[(INFO|WARN|WARNING|ERROR|DEBUG|TRACE)\]');
  final match = tagRe.firstMatch(line);
  if (match == null) {
    return TextSpan(
      text: line,
      style: TextStyle(color: theme.colorScheme.onSurface),
    );
  }
  Color tone;
  switch (match.group(1)) {
    case 'ERROR':
      tone = theme.colorScheme.error;
      break;
    case 'WARN':
    case 'WARNING':
      tone = ChainColors.warning;
      break;
    case 'INFO':
      tone = theme.colorScheme.primary;
      break;
    default:
      tone = theme.colorScheme.onSurfaceVariant;
  }
  return TextSpan(
    children: [
      TextSpan(
        text: line.substring(0, match.start),
        style: TextStyle(color: theme.colorScheme.onSurface),
      ),
      TextSpan(
        text: match.group(0),
        style: TextStyle(color: tone, fontWeight: FontWeight.w600),
      ),
      TextSpan(
        text: line.substring(match.end),
        style: TextStyle(color: theme.colorScheme.onSurface),
      ),
    ],
  );
}

/// Matches ANSI / VT100 escape sequences (colour codes, cursor
/// moves) plus stray control characters. Daemon logs written to a
/// file by older `fai` builds carried `\x1b[..m` colour codes that
/// rendered as garbled "special characters" in the viewer; we
/// strip them defensively so existing log files render clean even
/// though current builds no longer write ANSI to files.
final RegExp _ansiAndControl = RegExp(
  r'\x1B\[[0-9;?]*[ -/]*[@-~]|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]',
);

String _sanitizeLine(String line) => line.replaceAll(_ansiAndControl, '');

/// Read up to [maxLines] of [path], sanitising each line of ANSI /
/// control characters. When [fromTop] is true the *first*
/// [maxLines] are returned (config files, read top-down);
/// otherwise the *last* [maxLines] are returned (logs, tail-first).
/// Returns an empty list on any I/O failure — the viewer renders
/// an empty-state hint rather than throwing.
Future<List<String>> _readLines(
  String path,
  int maxLines, {
  bool fromTop = false,
}) async {
  try {
    final f = File(path);
    if (!await f.exists()) return const <String>[];
    final raw = await f.readAsString();
    final lines = raw.split('\n').map(_sanitizeLine).toList();
    // Drop the trailing blank line that `split` leaves behind
    // when the file ends with `\n`.
    if (lines.isNotEmpty && lines.last.isEmpty) {
      lines.removeLast();
    }
    if (lines.length <= maxLines) return lines;
    return fromTop
        ? lines.sublist(0, maxLines)
        : lines.sublist(lines.length - maxLines);
  } catch (_) {
    return const <String>[];
  }
}
