import 'dart:convert';

import 'package:scheduler_core/scheduler_core.dart' as core;
import 'package:scheduler_persistence/persistence.dart';

/// Export metadata shared with every writer.
final class ExportContext {
  /// Creates an export context.
  ExportContext({
    required this.ownerId,
    required String timezone,
    required this.version,
  }) : timezone = _requiredTrimmed(timezone, 'timezone');

  /// Owner scope being exported.
  final core.OwnerId ownerId;

  /// Timezone id used to interpret local export presentation.
  final String timezone;

  /// Export format/schema version.
  final int version;
}

/// Thrown when an export cannot complete.
final class ExportException implements Exception {
  /// Creates an export exception with a diagnostic [message].
  const ExportException(this.message);

  /// Diagnostic text for logs and tests.
  final String message;

  @override
  String toString() => 'ExportException: $message';
}

/// Thrown when repository reads fail during export.
final class ExportRepositoryException extends ExportException {
  /// Creates an exception wrapping a repository [failure].
  ExportRepositoryException(this.failure)
      : super('Repository export read failed: ${failure.runtimeType}');

  /// Repository failure returned by an adapter.
  final RepositoryFailure failure;
}

/// Writer interface for readable exports.
abstract interface class ExportWriter {
  /// Start writing an export for [context].
  Future<void> begin(ExportContext context);

  /// Write one task record.
  Future<void> writeTask(core.Task task);

  /// Finish writing the export.
  Future<void> end();
}

/// Factory signature for sink-backed export writers.
typedef ExportWriterFactory = ExportWriter Function(StringSink sink);

/// Registry that resolves export format names to writer factories.
final class ExportWriterRegistry {
  /// Creates a registry from [factories].
  ExportWriterRegistry({
    Map<String, ExportWriterFactory> factories =
        const <String, ExportWriterFactory>{},
  }) : _factories = Map<String, ExportWriterFactory>.fromEntries(
          factories.entries.map(
            (entry) => MapEntry(_normalizeFormat(entry.key), entry.value),
          ),
        );

  final Map<String, ExportWriterFactory> _factories;

  /// Default registry with JSON and CSV stub writers.
  factory ExportWriterRegistry.defaults() {
    return ExportWriterRegistry(
      factories: <String, ExportWriterFactory>{
        'json': JsonExportWriter.new,
        'csv': CsvExportWriter.new,
      },
    );
  }

  /// Available format names.
  Set<String> get formats => Set<String>.unmodifiable(_factories.keys);

  /// Register [factory] for [format].
  void register(String format, ExportWriterFactory factory) {
    _factories[_normalizeFormat(format)] = factory;
  }

  /// Create a writer for [format] using [sink].
  ExportWriter create(String format, StringSink sink) {
    final normalized = _normalizeFormat(format);
    final factory = _factories[normalized];
    if (factory == null) {
      throw ExportException('Unknown export format: $format');
    }
    return factory(sink);
  }
}

/// Coordinates repository reads and export writer calls.
final class ExportController {
  /// Creates an export controller using [taskRepository].
  ExportController({
    required TaskRepository taskRepository,
    ExportWriterRegistry? writerRegistry,
  })  : _taskRepository = taskRepository,
        _writerRegistry = writerRegistry ?? ExportWriterRegistry.defaults();

  final TaskRepository _taskRepository;
  final ExportWriterRegistry _writerRegistry;

  /// Create a writer for [format] and export owner-scoped tasks to [sink].
  Future<void> exportTasksToSink({
    required ExportContext context,
    required String format,
    required StringSink sink,
    int pageSize = 100,
  }) {
    final writer = _writerRegistry.create(format, sink);
    return exportTasks(
      context: context,
      writer: writer,
      pageSize: pageSize,
    );
  }

  /// Export all owner-scoped tasks through [writer].
  Future<void> exportTasks({
    required ExportContext context,
    required ExportWriter writer,
    int pageSize = 100,
  }) async {
    if (pageSize <= 0) {
      throw ArgumentError.value(pageSize, 'pageSize', 'Must be positive.');
    }

    await writer.begin(context);
    String? cursor;
    do {
      final result = await _taskRepository.findByOwner(
        ownerId: context.ownerId,
        page: core.PageRequest(cursor: cursor, limit: pageSize),
      );
      if (result.isLeft) {
        throw ExportRepositoryException(result.left);
      }
      final page = result.right;
      for (final task in page.items) {
        await writer.writeTask(task);
      }
      cursor = page.nextCursor;
    } while (cursor != null);
    await writer.end();
  }
}

/// Minimal JSON export writer stub.
final class JsonExportWriter implements ExportWriter {
  /// Creates a JSON writer that writes to the provided string sink.
  JsonExportWriter(this._sink);

  final StringSink _sink;
  ExportContext? _context;
  bool _hasTask = false;
  bool _ended = false;

  @override
  Future<void> begin(ExportContext context) async {
    _assertNotEnded();
    _context = context;
    _sink.write(
      '{"context":${jsonEncode(_contextToJson(context))},"tasks":[',
    );
  }

  @override
  Future<void> writeTask(core.Task task) async {
    final context = _requireContext();
    if (_hasTask) {
      _sink.write(',');
    }
    _sink.write(jsonEncode(core.TaskDocumentMapping.toDocument(
      task,
      ownerId: context.ownerId.value,
    )));
    _hasTask = true;
  }

  @override
  Future<void> end() async {
    _requireContext();
    _assertNotEnded();
    _sink.write(']}');
    _ended = true;
  }

  ExportContext _requireContext() {
    final context = _context;
    if (context == null) {
      throw const ExportException('Writer has not started.');
    }
    return context;
  }

  void _assertNotEnded() {
    if (_ended) {
      throw const ExportException('Writer has already ended.');
    }
  }
}

/// Minimal CSV export writer stub.
final class CsvExportWriter implements ExportWriter {
  /// Creates a CSV writer that writes to the provided string sink.
  CsvExportWriter(this._sink);

  final StringSink _sink;
  bool _started = false;
  bool _ended = false;

  @override
  Future<void> begin(ExportContext context) async {
    _assertNotEnded();
    _started = true;
    _sink.writeln(
      'id,title,projectId,type,status,durationMinutes,scheduledStartUtc,'
      'scheduledEndUtc',
    );
  }

  @override
  Future<void> writeTask(core.Task task) async {
    if (!_started) {
      throw const ExportException('Writer has not started.');
    }
    _assertNotEnded();
    _sink.writeln(<String>[
      task.id,
      task.title,
      task.projectId,
      core.PersistenceEnumMapping.encodeTaskType(task.type),
      core.PersistenceEnumMapping.encodeTaskStatus(task.status),
      task.durationMinutes?.toString() ?? '',
      task.scheduledStart == null
          ? ''
          : core.PersistenceDateTimeConvention.toStoredString(
              task.scheduledStart!,
            ),
      task.scheduledEnd == null
          ? ''
          : core.PersistenceDateTimeConvention.toStoredString(
              task.scheduledEnd!,
            ),
    ].map(_csvCell).join(','));
  }

  @override
  Future<void> end() async {
    if (!_started) {
      throw const ExportException('Writer has not started.');
    }
    _assertNotEnded();
    _ended = true;
  }

  void _assertNotEnded() {
    if (_ended) {
      throw const ExportException('Writer has already ended.');
    }
  }
}

Map<String, Object?> _contextToJson(ExportContext context) {
  return <String, Object?>{
    'ownerId': context.ownerId.value,
    'timezone': context.timezone,
    'version': context.version,
  };
}

String _csvCell(String value) {
  final needsQuotes =
      value.contains(',') || value.contains('"') || value.contains('\n');
  final escaped = value.replaceAll('"', '""');
  return needsQuotes ? '"$escaped"' : escaped;
}

String _normalizeFormat(String format) {
  return _requiredTrimmed(format, 'format').toLowerCase();
}

String _requiredTrimmed(String value, String name) {
  final trimmed = value.trim();
  if (trimmed.isEmpty) {
    throw ArgumentError.value(value, name, 'Value must not be blank.');
  }
  return trimmed;
}
