/**
 * This module defines the API endpoints for dealing with Accounts directly,
 * including any data-transfer objects that are needed.
 */
module account.api;

import handy_http_primitives;
import handy_http_data.json;
import handy_http_handlers.path_handler;
import std.datetime;

import profile.service;
import profile.data;
import profile.api : PROFILE_PATH;
import account.model;
import account.service;
import account.dto;
import util.money;
import util.pagination;
import util.data;
import account.data;
import attachment.data;
import attachment.dto;

const ACCOUNT_PATH = PROFILE_PATH ~ "/accounts/:accountId:ulong";

@GetMapping(PROFILE_PATH ~ "/accounts")
void handleGetAccounts(ref ServerHttpRequest request, ref ServerHttpResponse response) {
    import std.algorithm;
    import std.array;
    auto ds = getProfileDataSource(request);
    auto accounts = ds.getAccountRepository().findAll()
        .map!(a => AccountResponse.of(a, getBalance(ds, a.id))).array;
    writeJsonBody(response, accounts);
}

@GetMapping(ACCOUNT_PATH)
void handleGetAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
    ulong accountId = request.getPathParamAs!ulong("accountId");
    auto ds = getProfileDataSource(request);
    auto account = ds.getAccountRepository().findById(accountId)
        .orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND));
    writeJsonBody(response, AccountResponse.of(account, getBalance(ds, account.id)));
}

@PostMapping(PROFILE_PATH ~ "/accounts")
void handleCreateAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
    auto ds = getProfileDataSource(request);
    AccountCreationPayload payload = readJsonBodyAs!AccountCreationPayload(request);
    // TODO: Validate the account creation payload.
    AccountType type = AccountType.fromId(payload.type);
    Currency currency = Currency.ofCode(payload.currency);
    Account account = ds.getAccountRepository().insert(
        type,
        payload.numberSuffix,
        payload.name,
        currency,
        payload.description
    );
    writeJsonBody(response, AccountResponse.of(account, getBalance(ds, account.id)));
}

@PutMapping(ACCOUNT_PATH)
void handleUpdateAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
    ulong accountId = request.getPathParamAs!ulong("accountId");
    AccountCreationPayload payload = readJsonBodyAs!AccountCreationPayload(request);
    auto ds = getProfileDataSource(request);
    AccountRepository repo = ds.getAccountRepository();
    auto account = repo.findById(accountId).orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND));
    Account updated = repo.update(accountId, Account(
        account.id,
        account.createdAt,
        account.archived,
        AccountType.fromId(payload.type),
        payload.numberSuffix,
        payload.name,
        Currency.ofCode(payload.currency),
        payload.description
    ));
    writeJsonBody(response, AccountResponse.of(updated, getBalance(ds, updated.id)));
}

@DeleteMapping(ACCOUNT_PATH)
void handleDeleteAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
    ulong accountId = request.getPathParamAs!ulong("accountId");
    auto ds = getProfileDataSource(request);
    ds.getAccountRepository().deleteById(accountId);
}

@GetMapping(ACCOUNT_PATH ~ "/balance")
void handleGetAccountBalance(ref ServerHttpRequest request, ref ServerHttpResponse response) {
    ulong accountId = request.getPathParamAs!ulong("accountId");
    auto ds = getProfileDataSource(request);
    SysTime timestamp = Clock.currTime(UTC());
    string providedTimestamp = request.getParamAs!string("timestamp");
    if (providedTimestamp != null && providedTimestamp.length > 0) {
        timestamp = SysTime.fromISOExtString(providedTimestamp);
    }
    Optional!long balance = getBalance(ds, accountId, timestamp);
    if (balance.isNull) {
        response.writeBodyString("{\"balance\": null}", ContentTypes.APPLICATION_JSON);
    } else {
        import std.conv : to;
        response.writeBodyString("{\"balance\": " ~ balance.value.to!string ~ "}", ContentTypes.APPLICATION_JSON);
    }
}

@PathMapping(HttpMethod.GET, ACCOUNT_PATH ~ "/history")
void handleGetAccountHistory(ref ServerHttpRequest request, ref ServerHttpResponse response) {
    ulong accountId = request.getPathParamOrThrow!ulong("accountId");
    PageRequest pagination = PageRequest.parse(request, PageRequest(1, 10, [Sort("timestamp", SortDir.DESC)]));
    auto ds = getProfileDataSource(request);
    AccountRepository accountRepo = ds.getAccountRepository();
    auto page = accountRepo.getHistory(accountId, pagination);
    writeHistoryResponse(response, page);
}

private string serializeAccountHistoryItem(in AccountHistoryItemResponse i) {
    import asdf : serializeToJson;
    if (i.type == AccountHistoryItemType.JournalEntry) {
        return serializeToJson(cast(AccountHistoryJournalEntryItemResponse) i);
    } else if (i.type == AccountHistoryItemType.ValueRecord) {
        return serializeToJson(cast(AccountHistoryValueRecordItemResponse) i);
    } else {
        return serializeToJson(i);
    }
}

private void writeHistoryResponse(ref ServerHttpResponse response, in Page!AccountHistoryItemResponse page) {
    // Manual serialization of response due to inheritance structure.
    import asdf;
    import std.json;
    string initialJsonObj = serializeToJson(page);
    JSONValue obj = parseJSON(initialJsonObj);
    obj.object["items"] = JSONValue.emptyArray;
    foreach (item; page.items) {
        string initialItemJson = serializeAccountHistoryItem(item);
        obj.object["items"].array ~= parseJSON(initialItemJson);
    }

    string jsonStr = obj.toJSON();
    response.writeBodyString(jsonStr, ContentTypes.APPLICATION_JSON);
}

@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/account-balances")
void handleGetTotalBalances(ref ServerHttpRequest request, ref ServerHttpResponse response) {
    auto ds = getProfileDataSource(request);
    auto balances = getTotalBalanceForAllAccounts(ds);
    writeJsonBody(response, balances);
}

@GetMapping(PROFILE_PATH ~ "/accounts/export")
void handleGetAccountsExport(ref ServerHttpRequest request, ref ServerHttpResponse response) {
    import std.algorithm;
    import std.array;
    import std.conv : to;
    import util.csv;
    import streams;
    auto pc = getProfileContextOrThrow(request);
    auto ds = getProfileDataSource(pc);
    AccountResponse[] accounts = ds.getAccountRepository().findAll()
        .map!(a => AccountResponse.of(a, getBalance(ds, a.id))).array;
    
    response.headers.add("Content-Type", "text/csv");
    response.headers.add("Content-Disposition", "attachment; filename=accounts.csv");
    CsvStreamWriter!(OutputStream!(ubyte)*) csv = CsvStreamWriter!(OutputStream!(ubyte)*)(&response.outputStream);
    csv
        .append("ID")
        .append("Name")
        .append("Number Suffix")
        .append("Currency")
        .append("Type")
        .append("Balance")
        .append("Created At")
        .append("Archived")
        .append("Description")
        .newLine();
    
    foreach (account; accounts) {
        csv
            .append(account.id)
            .append(account.name)
            .append(account.numberSuffix)
            .append(account.currency.code)
            .append(account.type)
            .append(account.currentBalance
                .mapIfPresent!((b) {
                    auto money = MoneyValue(account.currency, b);
                    auto n = money.toFloatingPoint();
                    import std.format;
                    return format("%." ~ account.currency.fractionalDigits.to!string ~ "f", n);
                })
                .orElse("")
            )
            .append(account.createdAt)
            .append(account.archived)
            .append(account.description)
            .newLine();
    }
}

// Value records:

const PageRequest VALUE_RECORD_DEFAULT_PAGE_REQUEST = PageRequest(1, 10, [Sort("timestamp", SortDir.DESC)]);

@PathMapping(HttpMethod.GET, ACCOUNT_PATH ~ "/value-records")
void handleGetValueRecords(ref ServerHttpRequest request, ref ServerHttpResponse response) {
    ulong accountId = request.getPathParamAs!ulong("accountId");
    auto ds = getProfileDataSource(request);
    scope attachmentRepo = ds.getAttachmentRepository();
    auto page = ds.getAccountValueRecordRepository()
        .findAllByAccountId(accountId, PageRequest.parse(request, VALUE_RECORD_DEFAULT_PAGE_REQUEST))
        .mapTo!()((vr) => AccountValueRecordResponse.of(vr, attachmentRepo));
    writeJsonBody(response, page);
}

@PathMapping(HttpMethod.GET, ACCOUNT_PATH ~ "/value-records/:valueRecordId:ulong")
void handleGetValueRecord(ref ServerHttpRequest request, ref ServerHttpResponse response) {
    ulong accountId = request.getPathParamAs!ulong("accountId");
    ulong valueRecordId = request.getPathParamAs!ulong("valueRecordId");
    auto ds = getProfileDataSource(request);
    auto attachmentRepo = ds.getAttachmentRepository();
    auto record = ds.getAccountValueRecordRepository().findById(accountId, valueRecordId)
        .orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND));
    writeJsonBody(response, AccountValueRecordResponse.of(record, attachmentRepo));
}

@PathMapping(HttpMethod.POST, ACCOUNT_PATH ~ "/value-records")
void handleCreateValueRecord(ref ServerHttpRequest request, ref ServerHttpResponse response) {
    ulong accountId = request.getPathParamAs!ulong("accountId");
    ProfileDataSource ds = getProfileDataSource(request);
    Account account = ds.getAccountRepository().findById(accountId)
        .orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND));
    AccountValueRecordRepository valueRecordRepo = ds.getAccountValueRecordRepository();
    AttachmentRepository attachmentRepo = ds.getAttachmentRepository();
    auto fullPayload = parseMultipartFilesAndBody!ValueRecordCreationPayload(request);
    ValueRecordCreationPayload payload = fullPayload.payload;
    SysTime timestamp = SysTime.fromISOExtString(payload.timestamp);
    AccountValueRecordType type = AccountValueRecordType.BALANCE; // TODO: Support more types.
    ulong valueRecordId;
    ds.doTransaction(() {
        AccountValueRecord record = valueRecordRepo.insert(
            timestamp,
            account.id,
            type,
            payload.value,
            account.currency
        );
        foreach (attachment; fullPayload.files) {
            ulong attachmentId = attachmentRepo.save(
                timestamp, attachment.name, attachment.contentType, attachment.content);
            valueRecordRepo.linkAttachment(record.id, attachmentId);
        }
        valueRecordId = record.id;
    });
    writeJsonBody(
        response,
        AccountValueRecordResponse.of(
            valueRecordRepo.findById(accountId, valueRecordId).orElseThrow(),
            attachmentRepo
        )
    );
}

@PathMapping(HttpMethod.DELETE, ACCOUNT_PATH ~ "/value-records/:valueRecordId:ulong")
void handleDeleteValueRecord(ref ServerHttpRequest request, ref ServerHttpResponse response) {
    ulong accountId = request.getPathParamAs!ulong("accountId");
    ulong valueRecordId = request.getPathParamAs!ulong("valueRecordId");
    ProfileDataSource ds = getProfileDataSource(request);
    AccountValueRecordRepository valueRecordRepo = ds.getAccountValueRecordRepository();
    AttachmentRepository attachmentRepo = ds.getAttachmentRepository();
    AccountValueRecord valueRecord = valueRecordRepo.findById(accountId, valueRecordId)
        .orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND));
    ds.doTransaction(() {
        // First delete all attachments.
        foreach (a; attachmentRepo.findAllByValueRecordId(valueRecord.id)) {
            attachmentRepo.remove(a.id);
        }
        valueRecordRepo.deleteById(accountId, valueRecordId);
    });
}
