# API Documentation — Throne of Fractured Fates Novel Manager

## 1. Overview

Base URL: `/api/v1`

Format: JSON (`Content-Type: application/json`)

Auth: personal-use app, disarankan pakai single API key lewat header:
```
Authorization: Bearer <API_KEY>
```

### Konvensi Response

Sukses (single resource):
```json
{ "data": { ... } }
```

Sukses (list, dengan pagination):
```json
{
  "data": [ ... ],
  "meta": { "page": 1, "per_page": 20, "total": 143 }
}
```

Error:
```json
{
  "error": {
    "code": "NOT_FOUND",
    "message": "Character dengan id tersebut tidak ditemukan"
  }
}
```

Kode status umum: `200` OK, `201` Created, `204` No Content (delete), `400` Bad Request, `404` Not Found, `409` Conflict (misal FK constraint), `422` Unprocessable Entity (validasi gagal).

### Query Parameter Umum (semua endpoint list)

| Param | Fungsi |
|---|---|
| `page`, `per_page` | Pagination |
| `sort` | Nama field, prefix `-` untuk descending, misal `sort=-order_num` |
| `search` | Pencarian teks bebas pada field nama/judul/deskripsi |
| Field spesifik | Filter langsung, misal `?status=aktif`, `?faction_id=3` |

---

## 2. Chapters

Skema `Chapters` tidak lagi menyimpan `outline`/`content`/`word_count` langsung — field itu pindah ke tabel `Chapter_Versions` (lihat 2b) supaya histori edit tidak pernah hilang. Nomor urut juga tidak disimpan sebagai integer tetap — dipakai field `position` (float) yang dihitung ulang jadi nomor tampilan saat data diambil (lihat 2c).

**Chapters (skema baru):** id (PK), title, position (float), status, current_version_id (FK → Chapter_Versions), updated_at

### `GET /chapters`
Filter tambahan: `status`, `tag_id`. Sort default: `position` (ascending). Setiap item response menyertakan `display_number` (dihitung dari rank `position`, bukan disimpan di DB).

### `GET /chapters/:id`
Response dasar (tanpa relasi), isi outline/content diambil dari `current_version_id`.

### `GET /chapters/:id/full`
Chapter lengkap beserta semua relasinya — dipakai untuk halaman detail chapter.
```json
{
  "data": {
    "id": 12,
    "display_number": 12,
    "title": "Duel Seren vs Zephyra",
    "position": 12000,
    "status": "final",
    "current_version": {
      "id": 34,
      "version_number": 3,
      "word_count": 4200,
      "outline": "...",
      "content": "..."
    },
    "characters": [{ "id": 3, "name": "Seren", "role_in_chapter": "POV" }],
    "locations": [{ "id": 5, "name": "Hutan bekas pertarungan Morvath" }],
    "tags": [{ "id": 2, "label": "duel" }],
    "timeline_events": [{ "id": 8, "description": "...", "chrono_order": 14 }],
    "plot_threads": [{ "id": 4, "name": "Identitas pelaku kutukan Aria" }]
  }
}
```

### `POST /chapters`
```json
{
  "title": "string (required)",
  "status": "draft | dirapikan | final",
  "outline": "string",
  "content": "string",
  "insert_after_chapter_id": "integer (opsional — pengganti input position manual)",
  "insert_before_chapter_id": "integer (opsional, alternatif dari insert_after_chapter_id)"
}
```
Server menghitung `position` otomatis (rata-rata dua tetangga, atau +1000 kalau di ujung list), lalu membuat baris pertama di `Chapter_Versions` (`version_number = 1`) dari `outline`/`content` yang dikirim.

### `PUT /chapters/:id`
Untuk update metadata saja (title, status) — tidak membuat versi baru. Kalau `outline`/`content` ikut dikirim di body ini, server otomatis mengalihkan ke proses yang sama seperti `POST /chapters/:id/versions` (lihat 2b) supaya versi lama tetap tersimpan.

### `DELETE /chapters/:id`
Menghapus chapter beserta seluruh `Chapter_Versions` miliknya dan baris junction terkait (`chapter_links`, `chapter_tags`, `plot_mentions`); `timeline_events.chapter_id` dan `lore_entries.first_revealed_chapter_id` di-set null, bukan ikut terhapus.

---

## 2b. Chapter Versions

**Chapter_Versions:** id (PK), chapter_id (FK → Chapters), version_number (integer, auto increment per chapter), outline (text), content (text), word_count, created_at, note (opsional)

### `GET /chapters/:id/versions`
Daftar semua versi (nomor, tanggal, word count, note, preview singkat), urut terbaru dulu.

### `GET /chapters/:id/versions/:version_id`
Isi lengkap satu versi tertentu (outline + content penuh).

### `POST /chapters/:id/versions`
```json
{ "outline": "string", "content": "string", "note": "string (opsional)" }
```
Insert baris baru di `Chapter_Versions` dengan `version_number` naik satu, lalu memindahkan `Chapters.current_version_id` ke versi baru ini. Versi-versi sebelumnya tidak dihapus atau diubah.

### `POST /chapters/:id/restore/:version_id`
Menjadikan versi lama sebagai versi aktif kembali — cuma mengubah `Chapters.current_version_id`, tidak menghapus versi mana pun (tetap bisa restore maju/mundur kapan saja).

### `DELETE /chapters/:id/versions/:version_id`
Hapus permanen satu versi tertentu (opsional, untuk beres-beres). Gagal (`409`) kalau versi itu sedang jadi `current_version_id`.

---

## 2c. Reorder & Penomoran Otomatis Chapter

Nomor chapter yang dilihat user ("Chapter 7") **tidak pernah disimpan** — selalu dihitung dari rank `position` saat data diambil. Ini membuat sisip dan pindah urutan chapter jadi murah (biasanya cuma 1 baris berubah).

### `POST /chapters/:id/move`
```json
{ "after_chapter_id": 4 }
```
atau
```json
{ "before_chapter_id": 5 }
```
Server menghitung `position` baru sebagai rata-rata dua tetangga di posisi tujuan, lalu update `position` chapter ini saja. Dipakai untuk kasus "chapter 7 ternyata harusnya jadi chapter 5" maupun menyisipkan satu/banyak chapter baru di tengah (tiap chapter baru dikirim lewat `POST /chapters` dengan `insert_after_chapter_id` berbeda-beda, tidak saling mempengaruhi).

### `POST /chapters/rebalance-positions`
Operasi utilitas internal: menata ulang seluruh `position` chapter jadi berjarak rapi lagi (misal kembali ke kelipatan 1000) tanpa mengubah urutan atau nomor tampilan. Dipanggil otomatis oleh server kalau jarak antar `position` sudah terlalu rapat untuk disisipi lagi, atau bisa dipanggil manual dari halaman admin/settings.

---

## 3. Characters

### `GET /characters`
Filter tambahan: `race_id`, `family_id`, `faction_id`, `tier_sublevel_id`, `status`.

### `GET /characters/:id`
Data dasar karakter saja.

### `GET /characters/:id/full`
Profil lengkap — endpoint utama untuk halaman detail karakter.
```json
{
  "data": {
    "id": 3,
    "name": "Seren Valcaryn",
    "alias": null,
    "status": "hidup",
    "race": { "id": 1, "name": "Manusia" },
    "family": { "id": 2, "name": "Valcaryn", "family_type": "bangsawan" },
    "faction": { "id": 1, "name": "Kerajaan Lurn" },
    "current_position": { "title": "Ksatria pelindung pribadi Aria" },
    "tier": { "tier_name": "Ascendants", "sublevel": "Advance" },
    "aetherial_traits": [
      {
        "id": 1,
        "form_name": "Valkyrie",
        "grade": "belum ditentukan",
        "skills": [
          { "id": 5, "name": "Dominion Field" },
          { "id": 6, "name": "Golden Aegis" }
        ]
      }
    ],
    "generic_skills": [{ "id": 10, "name": "Mata Badai" }],
    "items": [{ "id": 2, "name": "Sol Aria", "tier_rank": "Divine" }],
    "relationships": [
      { "character": { "id": 4, "name": "Zephyra Valcaryn" }, "relation_type": "kakak-adik", "status": "rekonsiliasi" }
    ],
    "first_appearance_chapter": { "id": 2, "title": "..." }
  }
}
```

### `POST /characters`
```json
{
  "name": "string (required)",
  "alias": "string",
  "race_id": "integer",
  "family_id": "integer",
  "faction_id": "integer",
  "tier_sublevel_id": "integer",
  "status": "hidup | mati | tidak diketahui",
  "description": "string",
  "first_appearance_chapter_id": "integer"
}
```

### `PUT /characters/:id`, `DELETE /characters/:id`
Standar. Delete karakter akan gagal (`409 Conflict`) jika masih direferensikan sebagai `owner_id` di Items atau `leader_id` di Factions — harus dipindah/dihapus dulu relasinya (mencegah data yatim).

---

## 4. Families

### `GET /families`
Filter: `race_id`, `faction_id`, `family_type`.

### `GET /families/:id/members`
```json
{ "data": [{ "id": 1, "name": "Julian Valerius" }, { "id": 5, "name": "Aria Valerius" }] }
```

### `POST /families`
```json
{
  "name": "string (required)",
  "family_type": "bangsawan | rakyat biasa | klan | suku",
  "race_id": "integer (required)",
  "faction_id": "integer",
  "parent_family_id": "integer",
  "notes": "string"
}
```

### `PUT /families/:id`, `DELETE /families/:id`
Delete akan gagal jika masih ada `characters.family_id` yang merujuk ke sini.

---

## 5. Factions

### `GET /factions`
Filter: `type`, `base_location_id`.

### `GET /factions/:id/positions`
Daftar seluruh jabatan dalam faksi ini beserta pemegangnya saat ini.
```json
{
  "data": [
    { "position": "Raja", "hierarchy_level": 1, "current_holder": { "id": 9, "name": "Raja Lurn" } },
    { "position": "Putra Mahkota", "hierarchy_level": 2, "current_holder": { "id": 1, "name": "Julian Valerius" } }
  ]
}
```

### `POST /factions`
```json
{
  "name": "string (required)",
  "type": "kerajaan | organisasi | sindikat",
  "leader_id": "integer",
  "base_location_id": "integer"
}
```

### `PUT /factions/:id`, `DELETE /factions/:id`

---

## 6. Faction Positions

### `GET /factions/:faction_id/positions/all`
Daftar definisi jabatan (bukan siapa pemegangnya) — untuk dropdown saat assign karakter.

### `POST /faction-positions`
```json
{
  "faction_id": "integer (required)",
  "title": "string (required)",
  "hierarchy_level": "integer (required)"
}
```

### `PUT /faction-positions/:id`, `DELETE /faction-positions/:id`

---

## 7. Character Positions

Junction dengan histori — satu karakter bisa punya banyak baris (riwayat jabatan).

### `GET /characters/:character_id/positions`
Riwayat jabatan seorang karakter, urut dari terbaru.

### `POST /character-positions`
```json
{
  "character_id": "integer (required)",
  "position_id": "integer (required)",
  "start_chapter_id": "integer",
  "status": "aktif | mantan | dicabut"
}
```
Saat status baru di-POST dengan `status: aktif`, server otomatis mengubah baris lama karakter tersebut pada `position_id` yang sama menjadi `mantan` (mencegah dua status aktif tumpang tindih).

### `PUT /character-positions/:id`, `DELETE /character-positions/:id`

---

## 8. Tiers & Tier Sublevels

Tabel referensi — biasanya diisi sekali di awal (seeding), jarang berubah lewat API.

### `GET /tiers`
```json
{ "data": [{ "id": 1, "name": "Embers", "level_order": 1 }, ...] }
```

### `GET /tiers/:id/sublevels`
```json
{ "data": [{ "id": 1, "name": "Beginner", "sub_order": 1 }, { "id": 2, "name": "Advanced", "sub_order": 2 }, { "id": 3, "name": "Grandmaster", "sub_order": 3 }] }
```

### `POST /tiers`, `PUT /tiers/:id`, `DELETE /tiers/:id`
### `POST /tier-sublevels`, `PUT /tier-sublevels/:id`, `DELETE /tier-sublevels/:id`

### `POST /characters/:id/promote`
Endpoint utilitas: menaikkan sublevel karakter satu tingkat. Kalau sudah di Grandmaster, otomatis pindah ke Beginner tier berikutnya.
```json
{ "data": { "character_id": 3, "new_tier": "Ascendants", "new_sublevel": "Advance" } }
```

---

## 9. Skills

### `GET /skills`
Filter: `character_id`, `type` (`generic`/`aetherial`), `aetherial_trait_id`.

### `POST /skills`
```json
{
  "character_id": "integer (required)",
  "name": "string (required)",
  "description": "string",
  "type": "generic | aetherial (required)",
  "aetherial_trait_id": "integer (required jika type = aetherial)",
  "first_used_chapter_id": "integer"
}
```
Validasi server: jika `type = aetherial` tapi `aetherial_trait_id` kosong → `422`. Jika `type = generic` tapi `aetherial_trait_id` diisi → `422`.

### `PUT /skills/:id`, `DELETE /skills/:id`

---

## 10. Aetherial Traits

### `GET /aetherial-traits`
Filter: `character_id`, `grade_id`.

### `GET /aetherial-traits/:id`
Termasuk daftar skill turunannya (`skills` array).

### `POST /aetherial-traits`
```json
{
  "character_id": "integer (required)",
  "form_name": "string (required)",
  "nature": "string — deskripsi sifat/jenis trait",
  "grade_id": "integer",
  "awakening_chapter_id": "integer"
}
```

### `PUT /aetherial-traits/:id`
### `DELETE /aetherial-traits/:id`
Gagal (`409`) jika masih ada `skills.aetherial_trait_id` yang merujuk ke sini — hapus/pindahkan skill turunannya dulu.

---

## 11. Trait Grades

### `GET /trait-grades`
### `POST /trait-grades`
```json
{ "code": "string (required, misal S / SS / SSS)", "criteria_description": "string" }
```
### `PUT /trait-grades/:id`, `DELETE /trait-grades/:id`

---

## 12. Items

### `GET /items`
Filter: `owner_id`, `type`, `tier_rank`.

### `POST /items`
```json
{
  "name": "string (required)",
  "type": "senjata | artefak | consumable",
  "tier_rank": "string",
  "description": "string",
  "owner_id": "integer",
  "origin_note": "string"
}
```

### `PUT /items/:id`, `DELETE /items/:id`

### `POST /items/:id/transfer`
Utilitas untuk memindah kepemilikan (misal Kaelen meminjamkan pedang ke Seren).
```json
{ "new_owner_id": "integer (required)" }
```

---

## 13. Locations

### `GET /locations`
Filter: `parent_region`, `controlling_faction_id`.

### `POST /locations`
```json
{
  "name": "string (required)",
  "parent_region": "string",
  "description": "string",
  "controlling_faction_id": "integer"
}
```

### `PUT /locations/:id`, `DELETE /locations/:id`

---

## 14. Races / Species

### `GET /races`
### `POST /races`
```json
{
  "name": "string (required)",
  "category": "ras utama | monster | mitos",
  "traits_description": "string",
  "origin_location_id": "integer"
}
```
### `PUT /races/:id`, `DELETE /races/:id`

---

## 15. Relationships

### `GET /characters/:character_id/relationships`
Mengembalikan semua relasi karakter ini baik sebagai `character_a` maupun `character_b` (digabung, arah dinormalisasi di response).

### `POST /relationships`
```json
{
  "character_a_id": "integer (required)",
  "character_b_id": "integer (required)",
  "relation_type": "string (required — keluarga | rival | mentor | romance | dll)",
  "status": "string",
  "notes": "string"
}
```

### `PUT /relationships/:id`, `DELETE /relationships/:id`

---

## 16. Chapter Links (junction Chapters ↔ Characters)

### `GET /chapters/:chapter_id/characters`
### `POST /chapter-links`
```json
{
  "chapter_id": "integer (required)",
  "character_id": "integer (required)",
  "role_in_chapter": "string (opsional, misal POV / cameo)"
}
```
### `DELETE /chapter-links/:chapter_id/:character_id`

---

## 17. Timeline Events

### `GET /timeline`
Sort default: `chrono_order` (bukan `chapter_id`, karena flashback bisa membuat urutan chapter ≠ urutan kronologis cerita).

### `POST /timeline-events`
```json
{
  "description": "string (required)",
  "chrono_order": "integer (required)",
  "chapter_id": "integer",
  "in_universe_date": "string (opsional)"
}
```

### `PUT /timeline-events/:id`, `DELETE /timeline-events/:id`

---

## 18. Plot Threads

### `GET /plot-threads`
Filter: `status` (`aktif`/`terselesaikan`/`menggantung`).

### `GET /plot-threads/:id/chapters`
Semua chapter yang menyinggung thread ini.

### `POST /plot-threads`
```json
{ "name": "string (required)", "status": "aktif | terselesaikan | menggantung", "resolution_notes": "string" }
```

### `PUT /plot-threads/:id`, `DELETE /plot-threads/:id`

---

## 19. Plot Mentions (junction Chapters ↔ Plot Threads)

### `POST /plot-mentions`
```json
{ "chapter_id": "integer (required)", "plot_thread_id": "integer (required)" }
```
### `DELETE /plot-mentions/:chapter_id/:plot_thread_id`

---

## 20. Lore Entries

### `GET /lore-entries`
Filter: `category` (sistem kekuatan / sejarah / ras / geografi).

### `POST /lore-entries`
```json
{
  "category": "string (required)",
  "title": "string (required)",
  "content": "string (required)",
  "first_revealed_chapter_id": "integer"
}
```

### `PUT /lore-entries/:id`, `DELETE /lore-entries/:id`

---

## 21. Tags & Chapter Tags

### `GET /tags`
### `POST /tags`
```json
{ "label": "string (required)" }
```
### `DELETE /tags/:id`

### `POST /chapter-tags`
```json
{ "chapter_id": "integer (required)", "tag_id": "integer (required)" }
```
### `DELETE /chapter-tags/:chapter_id/:tag_id`

---

## 22. Search Global

### `GET /search?q=morvath`
Full-text search lintas tabel (Characters, Chapters, Lore_Entries, Items, Locations).
```json
{
  "data": {
    "characters": [{ "id": 7, "name": "Morvath Chronos" }],
    "chapters": [{ "id": 9, "title": "Pertarungan Seren vs Morvath" }],
    "lore_entries": [],
    "items": [],
    "locations": []
  }
}
```

---

## 23. Ringkasan Kode Error

| Code | Arti |
|---|---|
| `NOT_FOUND` | Resource dengan id tersebut tidak ada |
| `VALIDATION_ERROR` | Body request tidak lolos validasi (lihat `error.fields`) |
| `FK_CONFLICT` | Tidak bisa hapus/ubah karena masih direferensikan resource lain |
| `DUPLICATE` | Kombinasi unique (misal `chapter_id` + `character_id` di junction) sudah ada |
| `UNAUTHORIZED` | API key tidak valid atau tidak disertakan |
