# Calendula - Plan 06: ICS Import (v2.7, Branch 2 von 2)

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Die Lese-Hälfte des `.ics`-Themas, aufgesetzt auf den Writer aus
Branch 1 (`feat/ics-export`, gemerged in `release/v2.7.0`). Calendula parst
RFC-5545-Dateien und führt sie über zwei Wege ein: eine einzelne `VEVENT` öffnet
das vorausgefüllte Erstellen-Formular (Review vor dem Speichern), eine Datei mit
vielen Events geht in einen Bulk-Import mit Ziel-Kalender-Auswahl und
Ergebnis-Report. Damit schließt sich der Backup→Restore-Kreis für lokale
Kalender. Beide Branches landen in **einem** Release v2.7.0.
`./gradlew lint test assembleDebug` bleibt grün; Release erst nach
On-Device-Review.

**Architecture:** `IcsParser` lebt rein in `domain/ics/` neben `IcsWriter` —
kein Android, JVM-testbar, symmetrisch zum Writer. Ausgabe ist ein
`IcsParseResult` (`events: List<ParsedIcsEvent>` + `warnings: List<String>`).
`ParsedIcsEvent` ist die Parse-Variante von `IcsEvent` (gleiche Felder, aber
`uid: String?` — eine eingelesene `VEVENT` kann ohne UID kommen). Zwei Adapter:
`ParsedIcsEvent.toEventForm(zone)` (Einzel-Öffnen → `EventForm`) und eine
Repository-Methode `importEvents(targetCalendarId, events)` → `ImportSummary`
(Bulk). Datei-IO (`Uri` lesen, Intent) liegt in `data/ics/` bzw. der
Activity/Compose-Schicht; Routing 1-vs-viele entscheidet anhand der geparsten
Event-Anzahl.

**Liberal-in/strict-out (Leitprinzip):** unbekannte Properties, fremde
`VTIMEZONE`-Blöcke und `RECURRENCE-ID`-Ausnahmen werden **übersprungen und im
Report vermerkt**, nie still verschluckt; ein einzelnes kaputtes `VEVENT` lässt
den Rest der Datei durch.

**Leitentscheidungen:**

1. **Parse-Mechanik (Umkehr von `IcsText`):** zuerst **Unfolding** (CRLF +
   Space/Tab → wegfalten), dann pro Zeile `NAME[;params]:value` zerlegen,
   TEXT-Werte **unescapen** (`\\` `\;` `\,` `\n`). Eine reine, einzeln getestete
   Schicht (`IcsLineParser`), nicht ad hoc im Walker.
2. **Datum/Zeit-Parsing (Umkehr der Writer-Zeitzonenregel):**
   - `VALUE=DATE` (`YYYYMMDD`) → all-day, Instant auf UTC-Mitternacht (wie der
     Provider all-day speichert), exklusives `DTEND` bleibt exklusiv.
   - `…T…Z` → UTC-Instant.
   - `…T…` mit `TZID=<zone>` → lokale Wandzeit in der Zone, aufgelöst gegen die
     **OS-tz-Datenbank** (`TimeZone.of`); unbekannte/fehlende `TZID` →
     Gerätezone als Fallback (+ Warnung).
   - Kein `VTIMEZONE`-Parsing — `TZID` wird gegen die OS-DB aufgelöst (s. Branch-1
     Entscheidung); ein `VTIMEZONE`-Block wird übersprungen (Warnung nur, wenn
     seine `TZID` nicht in der OS-DB ist).
3. **Routing 1-vs-viele:** genau **eine** `VEVENT` → vorausgefülltes
   Erstellen-Formular (`ParsedIcsEvent.toEventForm`, `calendarId=null` →
   Formular wählt wie gehabt den zuletzt genutzten Kalender vor). **Mehr als
   eine** → Bulk-Import-Screen (Ziel-Kalender-Picker, nur beschreibbare). Leere
   Datei → freundlicher „nichts gefunden"-Hinweis.
4. **UID-Dedup beim Bulk-Import:** vor dem Insert die vorhandenen
   `Events.UID_2445` des Ziel-Kalenders lesen; eine eingelesene UID, die schon
   existiert, wird **übersprungen** (gezählt als „bereits vorhanden"). v1:
   skip-not-update — kein Überschreiben, das hält den Restore idempotent und
   verlustfrei. Events ohne UID bekommen beim Insert eine frische
   (`UUID@calendula`, wie `insertEvent`).
5. **Empfang via Intent:** Manifest-`ACTION_VIEW`/`SEND` mit MIME `text/calendar`
   (+ `.ics`-Pfadmuster für `file`/`content`-Schemes). `MainActivity`
   (`singleTop`, wie beim Reminder-Tap) liest den `Uri`, parst, und reicht das
   Ergebnis als Compose-State an `CalendarHost` (gleiches Key-Muster wie der
   Notification-Deep-Link).
6. **Reminder/Status/Transp zurück:** `VALARM` `TRIGGER` (negatives `-PT…`,
   `PT0…`) → Lead-Minuten; `STATUS`/`TRANSP` → `EventStatus`/`Availability`.
   `DURATION`-statt-`DTEND` über `parseRfc2445DurationMillis` (existiert aus
   Branch 1). Attendees werden **nicht** importiert (kein Modell; Warnung wenn
   vorhanden).

**Recherche-Befunde (Codebase, 2026-06-18 — aus Branch 1):**
- `IcsText.escapeText`/`foldLine` + `IcsWriter` existieren; Parser spiegelt sie.
- `parseRfc2445DurationMillis` (in `IcsExportMapper.kt`) parst die
  Provider-`DURATION`-Formen inkl. des nicht-standardkonformen `P<n>S`.
- `EventForm` (Domain): Zeiten als `LocalDateTime` in Gerätezone, `calendarId`
  nullable; das Formular wählt bei `null` den zuletzt genutzten Kalender vor.
- `insertEvent` schreibt bereits `Events.UID_2445`; für den Import muss eine
  **vorgegebene** UID durchgereicht werden (Insert-Variante / Parameter).

---

## Tasks

**Parser-Engine (`domain/ics/`, reine Kotlin, JVM-Tests):**
- [x] `IcsText`: `unescapeText` + `unfold(lines)` ergänzen (+ Test)
- [x] `IcsLineParser`: `NAME;PARAM=v;PARAM=v:VALUE` → (name, params-map, value);
      Param-Werte ggf. in Quotes; Test (Kantenfälle: Doppelpunkt im Wert,
      gequotete Params)
- [x] `ParsedIcsEvent` (wie `IcsEvent`, aber `uid: String?`) + Datum/Zeit-Parser
      (`VALUE=DATE` / `…Z` / `TZID` → Instant + isAllDay + zoneId)
- [x] `IcsParser.parse(text)` → `IcsParseResult(events, warnings)`: VCALENDAR/
      VEVENT-Walk, skip-and-report für `RECURRENCE-ID`/unbekannte `VTIMEZONE`/
      Attendees; ein defektes VEVENT killt nicht den Rest. **Round-trip-Test**
      gegen `IcsWriter`-Ausgabe (all-day, getimt, wiederkehrend+TZID, Reminder)
      + Fremd-Quirks (gefaltete Zeilen, fehlende UID, `PT…`-Trigger)

**Datenschicht (`data/calendar/` + `data/ics/`):**
- [x] `ParsedIcsEvent.toEventForm(zone)` (Einzel-Öffnen → `EventForm`); Test
- [x] Datasource: `existingUids(calendarId)` (Query `Events.UID_2445`) +
      `insertImported(event, calendarId)` (Insert mit vorgegebener/erzeugter UID)
- [x] Repository `importEvents(targetCalendarId, events)` → `ImportSummary`
      (imported / skippedDuplicate / skippedUnsupported); UID-Dedup; Test mit
      Fake-Datasource
- [x] `IcsImporter` (`data/ics/`): `Uri` → Text lesen (UTF-8, `contentResolver`)

**Intent + Routing:**
- [x] Manifest: `ACTION_VIEW`/`ACTION_SEND`, MIME `text/calendar`, `.ics`-
      Pfadmuster (`file`/`content`); `MainActivity` parst eingehenden `Uri`
- [x] Routing in `CalendarHost`: 1 Event → Erstellen-Formular vorausgefüllt;
      >1 → Bulk-Import-Screen; 0 → Hinweis

**UI:**
- [x] Bulk-Import-Screen: Ziel-Kalender-Picker (OptionCard, nur beschreibbare),
      Anzahl/Vorschau, Import-Button → `ImportSummary` als Ergebnis
- [x] Einzel-Öffnen: `EventEditScreen` mit vorausgefülltem Formular (neuer
      Prefill-Pfad im `EventEditViewModel`, ohne `eventId`)
- [x] Strings DE+EN: Import-Titel, Ziel-Auswahl, Ergebnis-Plurals
      (importiert / übersprungen-vorhanden / übersprungen-nicht-unterstützt),
      leere-Datei-Hinweis

**Abschluss:**
- [x] `./gradlew lint test assembleDebug` grün
- [ ] CHANGELOG done; ROADMAP/STATE pending; v2.7 cut **erst** wenn beide
      Branches gemerged sind und On-Device-Review durch ist
