# Event History Website: Implementation Handoff

This document is decision-complete. Implement it without re-discovering page, model, cache, profile, ranking, or routing conventions.

## Deliverables

```text
src/model/base/event-history.model.php
src/model/cms/event-history.model.php
src/pages/public/event-statistics/
├── event-statistics.controller.php
├── event-statistics.view.php
├── classes/
│   ├── EventStatisticsPresenter.php
│   └── EventStatisticsCsv.php
└── views/
    ├── events.view.php
    ├── guilds.view.php
    ├── players.view.php
    └── player.view.php
```

Also update:

- `src/pages/routes.php`: register public `event-statistics` with label `Event Statistics`.
- `src/core/translations/es.php` and `pt.php`: add every new `__()` key; English remains source text.
- This documentation only if implementation changes a contract described here.

Do not modify theme navigation, source-specific models, schema/DDL, or the existing profile/ranking pages.

## Model

Create abstract `EventHistoryBaseModel extends Model`; query `self::$connCMS`. Create thin CMS bridge `EventHistoryModel extends EventHistoryBaseModel` using `require_once MODELS_BASE.'event-history.model.php';`.

Required API:

```php
EventHistoryModel::getEvents(): array;
EventHistoryModel::getCompletedEvent(int $eventId): array|false;
EventHistoryModel::getGuilds(int $eventId): array;
EventHistoryModel::getGuild(int $eventId, int $guildId): array|false;
EventHistoryModel::getPlayers(int $eventId, int $guildId): array;
EventHistoryModel::getPlayer(int $eventId, int $guildId, int $playerId): array|false;
```

Rules:

- `getEvents()`: all rows ordered by `event_hour DESC, id DESC`.
- `getCompletedEvent()`: filter by `id` and `status='completed'`.
- Guild methods operate only inside one event and group/identify by `guild_id`; retain the snapshotted `guild_name`.
- `getGuilds()` returns `guild_id`, `guild_name`, and `player_count`, ordered by guild name with `guild_id=0` last.
- Player methods return every business snapshot column plus the four derived aliases from [data-contract.md](data-contract.md).
- `getPlayers()` orders by `kills DESC, deaths ASC, player_name ASC, player_id ASC`.
- `getPlayer()` must filter the full parent tuple: `event_id`, `guild_id`, `player_id`.
- Cast identifiers before binding. Do not interpolate request values or query live entity/guild tables.

## Controller and Classes

The main controller owns dispatch and joins the partials:

1. `loadModel('event-history')`; load `Cache`, presenter, and CSV classes.
2. Read identifiers only from `$_GET`; accept positive `event_id`/`player_id`, and non-negative `guild_id` because zero is valid.
3. Reject incomplete hierarchy: guild requires event; player requires event+guild; download requires event+guild.
4. Without `event_id`, load the event list and select `views/events.view.php`.
5. With `event_id`, load via `getCompletedEvent()`; absence includes the normal 404 page with HTTP 404.
6. With event only, load/select the guild list.
7. With event+guild, validate the guild before loading/selecting players.
8. With event+guild+player, load/select player detail.
9. `download=guild` takes precedence over HTML player rendering, validates the same parents, serves the CSV, then exits.
10. Any other `download` value returns 404.

Render the selected partial into `$eventStatisticsContent` with output buffering inside the controller. The shell view only opens the page/block, prints the correct title/content, and closes them. For an error, the shell includes `PAGES_FOLDER.'404.php'` instead of wrapping it as Event Statistics.

`EventStatisticsPresenter` is the only formatting/presentation utility:

- HTML escape all database values.
- Format seconds as unbounded `HH:MM:SS` using integer arithmetic; do not wrap at 24 hours.
- Return localized `No Guild` for `guild_id=0` and empty name.
- Build player/event/guild field arrays consumed by `ComponentsTemplate::printFields()`.
- Use the model aliases `kills`, `deaths`, `times_revived`, and `revives_given`; do not recalculate them differently in views.

`EventStatisticsCsv`:

- Accepts the validated event, guild, and rows; it must never re-resolve request IDs.
- Reads/writes the current `Cache` system using the permanent key defined in [page-contract.md](page-contract.md).
- Generates with `php://temp` + `fputcsv`, UTF-8 BOM, comma delimiter, and CRLF.
- Protect text values beginning with `=`, `+`, `-`, `@`, tab, or carriage return by prefixing an apostrophe.
- On response: clear the active output buffer, set `Content-Type: text/csv; charset=UTF-8`, safe attachment filename, `Content-Length`, and `X-Content-Type-Options: nosniff`; echo and exit.

## Views

- Shell: `ComponentsTemplate::StartPage()`, `PageTitle(__('Event Statistics'))`, one shared block, content, end block/page.
- Events: common ranking table with Event, Date, Status. Completed event name is linked; `in_progress` is plain text. No download column.
- Guilds: common ranking table with Guild and Players. Guild name is the link. Show `No Guild` for ID zero.
- Players: common ranking table with Name, Time, Kills, Deaths, Times Revived, Revives Given. Add one guild CSV button above the table. Player name links to its strict parent hierarchy.
- Player: profile-style `fake-table` rows via `ComponentsTemplate::printFields()`. Include event name/date, player ID/name, guild ID/name with backlink, formatted time, all totals, and every raw kill/death/revive value. No avatar or live profile link.
- Empty completed event/guild lists show a localized info alert rather than malformed tables.

Use relative/localized links consistently:

```text
event-statistics?event_id={eventId}
event-statistics?event_id={eventId}&guild_id={guildId}
event-statistics?event_id={eventId}&guild_id={guildId}&player_id={playerId}
event-statistics?event_id={eventId}&guild_id={guildId}&download=guild
```

## Acceptance Checklist

- Root list shows newest events first and exposes no CSV.
- `in_progress` row is visible but not linked; manually addressing it returns the standard 404 page/status.
- Completed event drills down through guild, player list, and detail without live-data access.
- `guild_id=0` is listed last, links correctly, caches correctly, and exports correctly.
- Cross-event guild/player IDs and incomplete/invalid query hierarchies return 404.
- Player list totals and order match the formulas in the data contract.
- Durations `0`, `59`, `3600`, `90061`, and values over 99 hours format without day wrapping.
- Guild/player HTML is reused for 900 seconds with language-separated keys.
- First guild CSV request creates one permanent cache file; later requests return identical bytes.
- CSV contains the exact ordered columns, raw fields, totals, BOM, comma separators, CRLF, and formula-safe text.
- No Jail-time field, event-wide CSV, player CSV, theme link, schema change, or source-specific model is introduced.
- Do not run a formatter or automated test suite for this task.
