part of 'main.dart';

const ink = Color(0xFF18332D);
const sage = Color(0xFF2F6B5B);
const mint = Color(0xFFE7F1EC);
const cream = Color(0xFFF7F5EF);
const orange = Color(0xFFE89552);
const everyonePersonId = '_everyone';
const defaultJarvisApiUrl = 'https://api-test.a-web.hu';
const legacyJarvisApiUrl = 'http://80.211.202.74';
const appVersionLabel = 'v1.0.0+1';

const avatarPresets = [
  AvatarPreset('boy', 'Fi\u00fa', '\u{1f9d2}'),
  AvatarPreset('girl', 'L\u00e1ny', '\u{1f467}'),
  AvatarPreset('man', 'F\u00e9rfi', '\u{1f468}'),
  AvatarPreset('woman', 'N\u0151', '\u{1f469}'),
  AvatarPreset('dog', 'Kutya', '\u{1f436}'),
  AvatarPreset('cat', 'Macska', '\u{1f431}'),
  AvatarPreset('snowman', 'H\u00f3ember', '\u2603\ufe0f'),
  AvatarPreset('strawberry', 'Eper', '\u{1f353}'),
  AvatarPreset('sun', 'Napocska', '\u2600\ufe0f'),
  AvatarPreset('flower', 'Vir\u00e1g', '\u{1f338}'),
  AvatarPreset('car', 'Aut\u00f3', '\u{1f697}'),
  AvatarPreset('rocket', 'Rak\u00e9ta', '\u{1f680}'),
];

const personColors = [
  Color(0xFF7189C8),
  Color(0xFFE89552),
  Color(0xFF9B78B4),
  Color(0xFF79A894),
  Color(0xFFD65A6F),
  Color(0xFFD6A74D),
  Color(0xFF4C9BCF),
  Color(0xFF6A9F58),
];

class AvatarPreset {
  const AvatarPreset(this.id, this.label, this.symbol);

  final String id;
  final String label;
  final String symbol;
}

class AppThemeStyle {
  const AppThemeStyle({
    required this.id,
    required this.name,
    required this.description,
    required this.seed,
    required this.surface,
  });

  final String id;
  final String name;
  final String description;
  final Color seed;
  final Color surface;
}

const appThemes = [
  AppThemeStyle(
    id: 'sage',
    name: 'Zs\u00e1lya otthon',
    description: 'Nyugodt, term\u00e9szetes csal\u00e1di alapst\u00edlus.',
    seed: sage,
    surface: cream,
  ),
  AppThemeStyle(
    id: 'berry',
    name: 'Eperkr\u00e9m',
    description:
        'Melegebb, j\u00e1t\u00e9kosabb sz\u00ednek gyerekes csal\u00e1dokhoz.',
    seed: Color(0xFFD65A6F),
    surface: Color(0xFFFFF5F6),
  ),
  AppThemeStyle(
    id: 'sky',
    name: 'Tiszta \u00e9g',
    description:
        'H\u0171v\u00f6sebb, rendezett, napt\u00e1rk\u00f6zpont\u00fa hangulat.',
    seed: Color(0xFF4C9BCF),
    surface: Color(0xFFF1F7FB),
  ),
  AppThemeStyle(
    id: 'sunny',
    name: 'Naps\u00e1rga',
    description:
        'Vil\u00e1gos, energikus, konyhai \u00e9s heti tervez\u0151s \u00e9rzet.',
    seed: Color(0xFFD6A74D),
    surface: Color(0xFFFFFAED),
  ),
];

class AppJsonStore {
  Future<File> _file() async {
    try {
      final dir = await getApplicationSupportDirectory();
      return File('${dir.path}${Platform.pathSeparator}kozos_ter.json');
    } on MissingPluginException {
      return File(
        '${Directory.systemTemp.path}${Platform.pathSeparator}kozos_ter.json',
      );
    } on UnsupportedError {
      return File(
        '${Directory.systemTemp.path}${Platform.pathSeparator}kozos_ter.json',
      );
    }
  }

  Future<AppSettings> load() async {
    try {
      final file = await _file();
      if (!await file.exists()) {
        final settings = AppSettings(
          people: defaultPeople(),
          themeId: 'sage',
          googleConnections: defaultGoogleConnections(),
          syncedEvents: const [],
          todoSuggestions: const [],
          jarvisSettings: JarvisSettings.defaults(),
        );
        await save(settings);
        return settings;
      }

      final raw = await file.readAsString();
      final json = jsonDecode(raw) as Map<String, dynamic>;
      return AppSettings.fromJson(json);
    } catch (_) {
      return AppSettings(
        people: defaultPeople(),
        themeId: 'sage',
        googleConnections: defaultGoogleConnections(),
        syncedEvents: const [],
        todoSuggestions: const [],
        jarvisSettings: JarvisSettings.defaults(),
      );
    }
  }

  Future<void> save(AppSettings settings) async {
    final file = await _file();
    await file.parent.create(recursive: true);
    const encoder = JsonEncoder.withIndent('  ');
    await file.writeAsString(encoder.convert(settings.toJson()));
  }
}

class AppSettings {
  AppSettings({
    required this.people,
    required this.themeId,
    required this.googleConnections,
    required this.syncedEvents,
    required this.todoSuggestions,
    required this.jarvisSettings,
  });

  final List<Person> people;
  final String themeId;
  final List<GoogleCalendarConnection> googleConnections;
  final List<CalendarEvent> syncedEvents;
  final List<String> todoSuggestions;
  final JarvisSettings jarvisSettings;

  factory AppSettings.fromJson(Map<String, dynamic> json) {
    final rawPeople = json['people'];
    final rawConnections = json['googleConnections'];
    final rawSyncedEvents = json['syncedEvents'];
    final rawTodoSuggestions = json['todoSuggestions'];
    final rawJarvisSettings = json['jarvisSettings'];
    return AppSettings(
      people: rawPeople is List
          ? normalizeDefaultPeople(
              rawPeople
                  .whereType<Map<String, dynamic>>()
                  .map(Person.fromJson)
                  .toList(),
            )
          : defaultPeople(),
      themeId: json['themeId'] as String? ?? 'sage',
      googleConnections: rawConnections is List
          ? rawConnections
                .whereType<Map<String, dynamic>>()
                .map(GoogleCalendarConnection.fromJson)
                .toList()
          : defaultGoogleConnections(),
      syncedEvents: rawSyncedEvents is List
          ? rawSyncedEvents
                .whereType<Map<String, dynamic>>()
                .map(CalendarEvent.fromJson)
                .toList()
          : const [],
      todoSuggestions: rawTodoSuggestions is List
          ? rawTodoSuggestions.whereType<String>().toList()
          : const [],
      jarvisSettings: rawJarvisSettings is Map<String, dynamic>
          ? JarvisSettings.fromJson(rawJarvisSettings)
          : JarvisSettings.defaults(),
    );
  }

  Map<String, dynamic> toJson() => {
    'themeId': themeId,
    'people': people.map((person) => person.toJson()).toList(),
    'googleConnections': googleConnections
        .map((connection) => connection.toJson())
        .toList(),
    'syncedEvents': syncedEvents.map((event) => event.toJson()).toList(),
    'todoSuggestions': todoSuggestions,
    'jarvisSettings': jarvisSettings.toJson(),
  };
}

class JarvisSettings {
  JarvisSettings({
    required this.apiUrl,
    required this.apiToken,
    this.deviceName,
    this.deviceId,
    this.deviceToken,
    this.registrationStatus = 'notRegistered',
  });

  final String apiUrl;
  final String apiToken;
  final String? deviceName;
  final int? deviceId;
  final String? deviceToken;
  final String registrationStatus;

  factory JarvisSettings.defaults() =>
      JarvisSettings(apiUrl: defaultJarvisApiUrl, apiToken: '');

  factory JarvisSettings.fromJson(Map<String, dynamic> json) => JarvisSettings(
    apiUrl: (json['apiUrl'] as String?) == legacyJarvisApiUrl
        ? defaultJarvisApiUrl
        : json['apiUrl'] as String? ?? JarvisSettings.defaults().apiUrl,
    apiToken: json['apiToken'] as String? ?? '',
    deviceName: json['deviceName'] as String?,
    deviceId: json['deviceId'] as int?,
    deviceToken: json['deviceToken'] as String?,
    registrationStatus:
        json['registrationStatus'] as String? ?? 'notRegistered',
  );

  JarvisSettings copyWith({
    String? apiUrl,
    String? apiToken,
    String? deviceName,
    int? deviceId,
    String? deviceToken,
    String? registrationStatus,
    bool clearDevice = false,
  }) {
    return JarvisSettings(
      apiUrl: apiUrl ?? this.apiUrl,
      apiToken: apiToken ?? this.apiToken,
      deviceName: clearDevice ? null : deviceName ?? this.deviceName,
      deviceId: clearDevice ? null : deviceId ?? this.deviceId,
      deviceToken: clearDevice ? null : deviceToken ?? this.deviceToken,
      registrationStatus: clearDevice
          ? 'notRegistered'
          : registrationStatus ?? this.registrationStatus,
    );
  }

  Map<String, dynamic> toJson() => {
    'apiUrl': apiUrl,
    'apiToken': apiToken,
    'deviceName': deviceName,
    'deviceId': deviceId,
    'deviceToken': deviceToken,
    'registrationStatus': registrationStatus,
  };
}

class GoogleCalendarConnection {
  GoogleCalendarConnection({
    required this.id,
    required this.accountEmail,
    required this.enabledCalendarIds,
    this.calendarPersonIds = const {},
    required this.lastSync,
  });

  final String id;
  final String accountEmail;
  final List<String> enabledCalendarIds;
  final Map<String, String> calendarPersonIds;
  final DateTime? lastSync;

  GoogleCalendarConnection copyWith({
    String? accountEmail,
    List<String>? enabledCalendarIds,
    Map<String, String>? calendarPersonIds,
    DateTime? lastSync,
  }) {
    return GoogleCalendarConnection(
      id: id,
      accountEmail: accountEmail ?? this.accountEmail,
      enabledCalendarIds: enabledCalendarIds ?? this.enabledCalendarIds,
      calendarPersonIds: calendarPersonIds ?? this.calendarPersonIds,
      lastSync: lastSync ?? this.lastSync,
    );
  }

  factory GoogleCalendarConnection.fromJson(Map<String, dynamic> json) {
    final rawCalendarIds = json['enabledCalendarIds'];
    final rawCalendarPersonIds = json['calendarPersonIds'];
    return GoogleCalendarConnection(
      id: json['id'] as String? ?? uniqueId(),
      accountEmail: json['accountEmail'] as String? ?? '',
      enabledCalendarIds: rawCalendarIds is List
          ? rawCalendarIds.whereType<String>().toList()
          : const [],
      calendarPersonIds: rawCalendarPersonIds is Map
          ? rawCalendarPersonIds.map(
              (key, value) => MapEntry(key.toString(), value.toString()),
            )
          : const {},
      lastSync: DateTime.tryParse(json['lastSync'] as String? ?? ''),
    );
  }

  Map<String, dynamic> toJson() => {
    'id': id,
    'accountEmail': accountEmail,
    'enabledCalendarIds': enabledCalendarIds,
    'calendarPersonIds': calendarPersonIds,
    'lastSync': lastSync?.toIso8601String(),
  };
}

class GoogleCalendarOption {
  const GoogleCalendarOption({
    required this.id,
    required this.name,
    required this.description,
    required this.color,
  });

  final String id;
  final String name;
  final String description;
  final Color color;
}

const availableGoogleCalendars = [
  GoogleCalendarOption(
    id: 'primary',
    name: 'Saj\u00e1t napt\u00e1r',
    description: 'A f\u0151 Google Calendar esem\u00e9nyeid.',
    color: Color(0xFF4285F4),
  ),
  GoogleCalendarOption(
    id: 'family',
    name: 'Csal\u00e1di napt\u00e1r',
    description:
        'K\u00f6z\u00f6s csal\u00e1di programok \u00e9s eml\u00e9keztet\u0151k.',
    color: Color(0xFF34A853),
  ),
  GoogleCalendarOption(
    id: 'school',
    name: 'Ovi / iskola',
    description:
        'Iskolai, \u00f3vodai \u00e9s k\u00fcl\u00f6n\u00f3r\u00e1s esem\u00e9nyek.',
    color: Color(0xFFFBBC05),
  ),
  GoogleCalendarOption(
    id: 'work',
    name: 'Munka',
    description:
        'Munkahelyi id\u0151pontok, ha szeretn\u00e9d l\u00e1tni \u0151ket.',
    color: Color(0xFF9B78B4),
  ),
];

List<GoogleCalendarConnection> defaultGoogleConnections() => [];

class Person {
  Person({
    required this.id,
    required this.name,
    required this.colorValue,
    required this.avatar,
    this.nextEventLimit = 1,
  });

  final String id;
  final String name;
  final int colorValue;
  final String avatar;
  final int nextEventLimit;

  Color get color => Color(colorValue);

  Person copyWith({
    String? name,
    int? colorValue,
    String? avatar,
    int? nextEventLimit,
  }) {
    return Person(
      id: id,
      name: name ?? this.name,
      colorValue: colorValue ?? this.colorValue,
      avatar: avatar ?? this.avatar,
      nextEventLimit: nextEventLimit ?? this.nextEventLimit,
    );
  }

  factory Person.fromJson(Map<String, dynamic> json) {
    return Person(
      id: json['id'] as String? ?? uniqueId(),
      name: json['name'] as String? ?? 'Csal\u00e1dtag',
      colorValue: json['colorValue'] as int? ?? personColors.first.toARGB32(),
      avatar: json['avatar'] as String? ?? avatarPresets.first.symbol,
      nextEventLimit: json['nextEventLimit'] as int? ?? 1,
    );
  }

  Map<String, dynamic> toJson() => {
    'id': id,
    'name': name,
    'colorValue': colorValue,
    'avatar': avatar,
    'nextEventLimit': nextEventLimit,
  };
}

List<Person> defaultPeople() => [];

List<Person> normalizeDefaultPeople(List<Person> current) {
  const seededIds = {'zsolt', 'eniko', 'balint', 'emilia', 'noemi'};
  return current.where((person) => !seededIds.contains(person.id)).toList();
}

class CalendarEvent {
  CalendarEvent(
    this.title,
    this.start,
    this.end,
    this.color, {
    required this.date,
    this.place = '',
    this.personId,
    this.externalId,
    this.calendarId,
    this.calendarName,
    this.note = '',
    this.hasTodos = false,
    this.todos = const [],
    this.source = EventSource.local,
  });

  final String title;
  final String start;
  final String end;
  final Color color;
  final String place;
  final String? personId;
  final DateTime date;
  final String? externalId;
  final String? calendarId;
  final String? calendarName;
  final String note;
  final bool hasTodos;
  final List<String> todos;
  final EventSource source;

  bool get isSynced => source == EventSource.deviceCalendar;
  bool get isUncategorized => personId == null;
  bool get isForEveryone => personId == everyonePersonId;

  CalendarEvent copyWith({
    String? title,
    String? start,
    String? end,
    Color? color,
    String? place,
    String? personId,
    DateTime? date,
    String? note,
    bool? hasTodos,
    List<String>? todos,
    bool clearPerson = false,
  }) {
    return CalendarEvent(
      title ?? this.title,
      start ?? this.start,
      end ?? this.end,
      color ?? this.color,
      date: date ?? this.date,
      place: place ?? this.place,
      personId: clearPerson ? null : personId ?? this.personId,
      externalId: externalId,
      calendarId: calendarId,
      calendarName: calendarName,
      note: note ?? this.note,
      hasTodos: hasTodos ?? this.hasTodos,
      todos: todos ?? this.todos,
      source: source,
    );
  }

  factory CalendarEvent.fromJson(Map<String, dynamic> json) {
    return CalendarEvent(
      json['title'] as String? ?? 'N\u00e9vtelen esem\u00e9ny',
      json['start'] as String? ?? '00:00',
      json['end'] as String? ?? '',
      Color(json['colorValue'] as int? ?? sage.toARGB32()),
      date: DateTime.tryParse(json['date'] as String? ?? '') ?? DateTime.now(),
      place: json['place'] as String? ?? '',
      personId: json['personId'] as String?,
      externalId: json['externalId'] as String?,
      calendarId: json['calendarId'] as String?,
      calendarName: json['calendarName'] as String?,
      note: json['note'] as String? ?? '',
      hasTodos: json['hasTodos'] as bool? ?? false,
      todos: json['todos'] is List
          ? (json['todos'] as List).whereType<String>().toList()
          : const [],
      source: EventSource.values.firstWhere(
        (source) => source.name == json['source'],
        orElse: () => EventSource.local,
      ),
    );
  }

  factory CalendarEvent.fromDeviceEvent(
    device_calendar.Event event,
    device_calendar.Calendar calendar, {
    String? personId,
  }) {
    final startDate = event.start?.toLocal() ?? DateTime.now();
    final endDate = event.end?.toLocal();
    return CalendarEvent(
      event.title?.trim().isNotEmpty == true
          ? event.title!.trim()
          : 'N\u00e9vtelen napt\u00e1resem\u00e9ny',
      event.allDay == true ? 'Eg\u00e9sz nap' : formatTime(startDate),
      event.allDay == true || endDate == null ? '' : formatTime(endDate),
      Color(calendar.color ?? const Color(0xFF4285F4).toARGB32()),
      date: DateTime(startDate.year, startDate.month, startDate.day),
      place: [
        calendar.name ?? 'Napt\u00e1r',
        if ((event.location ?? '').trim().isNotEmpty) event.location!.trim(),
      ].join(' \u00b7 '),
      externalId: event.eventId,
      calendarId: calendar.id,
      calendarName: calendar.name,
      personId: personId,
      note: event.description ?? '',
      source: EventSource.deviceCalendar,
    );
  }

  Map<String, dynamic> toJson() => {
    'title': title,
    'start': start,
    'end': end,
    'colorValue': color.toARGB32(),
    'date': date.toIso8601String(),
    'place': place,
    'personId': personId,
    'externalId': externalId,
    'calendarId': calendarId,
    'calendarName': calendarName,
    'note': note,
    'hasTodos': hasTodos,
    'todos': todos,
    'source': source.name,
  };
}

enum EventSource { local, deviceCalendar }

class ShoppingItem {
  ShoppingItem(this.name, this.amount, this.category, {this.done = false});

  final String name;
  final String amount;
  final String category;
  bool done;
}

class StockItem {
  StockItem(this.name, this.amount, this.unit, this.location, this.expiry);

  String name;
  int amount;
  String unit;
  String location;
  String expiry;
}

class JarvisProduct {
  const JarvisProduct({
    required this.barcode,
    required this.name,
    this.brand,
    this.quantity,
    this.imageUrl,
    this.source,
    this.fromCache = false,
  });

  final String barcode;
  final String name;
  final String? brand;
  final String? quantity;
  final String? imageUrl;
  final String? source;
  final bool fromCache;

  factory JarvisProduct.fromJson(Map<String, dynamic> json) => JarvisProduct(
    barcode: json['barcode'] as String? ?? '',
    name: json['name'] as String? ?? '',
    brand: json['brand'] as String?,
    quantity: json['quantity'] as String?,
    imageUrl: json['imageUrl'] as String?,
    source: json['source'] as String?,
    fromCache: json['fromCache'] as bool? ?? false,
  );

  String get displayName {
    final parts = [
      name,
      if (brand != null && brand!.trim().isNotEmpty) brand!,
      if (quantity != null && quantity!.trim().isNotEmpty) quantity!,
    ];
    return parts.join(' \u00b7 ');
  }
}

class WeatherSnapshot {
  const WeatherSnapshot({
    required this.locationName,
    required this.temperature,
    required this.description,
    required this.icon,
  });

  final String locationName;
  final double? temperature;
  final String description;
  final IconData icon;

  String get temperatureLabel =>
      temperature == null ? '\u2014' : '${temperature!.round()}\u00b0C';
}

class WeatherService {
  Future<WeatherSnapshot> loadCurrentWeather() async {
    try {
      var permission = await Geolocator.checkPermission();
      if (permission == LocationPermission.denied) {
        permission = await Geolocator.requestPermission();
      }
      if (permission == LocationPermission.denied ||
          permission == LocationPermission.deniedForever) {
        return const WeatherSnapshot(
          locationName: 'Helymeghat\u00e1roz\u00e1s n\u00e9lk\u00fcl',
          temperature: null,
          description: 'id\u0151j\u00e1r\u00e1s nem el\u00e9rhet\u0151',
          icon: Icons.location_off_outlined,
        );
      }

      final position = await Geolocator.getCurrentPosition(
        locationSettings: const LocationSettings(
          accuracy: LocationAccuracy.low,
          timeLimit: Duration(seconds: 8),
        ),
      );
      final locationName = await _locationName(position);
      final uri = Uri.https('api.open-meteo.com', '/v1/forecast', {
        'latitude': position.latitude.toStringAsFixed(4),
        'longitude': position.longitude.toStringAsFixed(4),
        'current': 'temperature_2m,weather_code',
      });
      final response = await http.get(uri).timeout(const Duration(seconds: 8));
      if (response.statusCode != 200) {
        throw HttpException('weather ${response.statusCode}');
      }

      final payload = jsonDecode(response.body) as Map<String, dynamic>;
      final current = payload['current'] as Map<String, dynamic>?;
      final temperature = (current?['temperature_2m'] as num?)?.toDouble();
      final code = (current?['weather_code'] as num?)?.toInt();
      return WeatherSnapshot(
        locationName: locationName,
        temperature: temperature,
        description: weatherDescription(code),
        icon: weatherIcon(code),
      );
    } catch (_) {
      return const WeatherSnapshot(
        locationName: 'Offline m\u00f3d',
        temperature: null,
        description: 'id\u0151j\u00e1r\u00e1s k\u00e9s\u0151bb friss\u00fcl',
        icon: Icons.cloud_off_outlined,
      );
    }
  }

  Future<String> _locationName(Position position) async {
    try {
      final places = await geocoding.Geocoding().placemarkFromCoordinates(
        position.latitude,
        position.longitude,
      );
      final place = places.firstOrNull;
      return [
        place?.locality,
        if (place?.locality == null) place?.subAdministrativeArea,
      ].whereType<String>().where((part) => part.isNotEmpty).join(', ');
    } catch (_) {
      return 'Aktu\u00e1lis hely';
    }
  }
}
