//   Copyright 2020 Vircadia Contributors
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.

'use strict';

import { Request } from 'express';

import { Accounts } from '@Entities/Accounts';
import { AccountEntity } from '@Entities/AccountEntity';

import { Domains } from '@Entities/Domains';
import { DomainEntity } from '@Entities/DomainEntity';

import { Places } from '@Entities/Places';
import { PlaceEntity } from '@Entities/PlaceEntity';

import { GenericFilter } from '@Entities/EntityFilters/GenericFilter';

import { createPublicKey } from 'crypto';
import { VKeyedCollection, VKeyValue } from '@Tools/vTypes';
import { IsNotNullOrEmpty, IsNullOrEmpty } from '@Tools/Misc';
import { Logger } from '@Tools/Logging';

import { Maturity } from '@Entities/Sets/Maturity';
import { Visibility } from '@Entities/Sets/Visibility';

// The public_key is sent as a binary (DER) form of a PKCS1 key.
// To keep backward compatibility, we convert the PKCS1 key into a SPKI key in PEM format
//      ("PEM" format is "Privacy Enhanced Mail" format and has the "BEGIN" and "END" text included).
export function convertBinKeyToPEM(pBinKey: Buffer): string {
    // Convert the passed binary into a crypto.KeyObject
    const publicKey = createPublicKey( {
        key: pBinKey,
        format: 'der',
        type: 'pkcs1'
    });
    // Convert the public key to 'SubjectPublicKeyInfo' (SPKI) format as a PEM string
    const convertedKey = publicKey.export({ type: 'spki', format: 'pem' });
    return convertedKey as string;
};

// The legacy interface returns public keys as a stripped PEM key.
// "stripped" in that the bounding "BEGIN" and "END" lines have been removed.
// This routine returns a stripped key string from a properly PEM formatted public key string.
export function createSimplifiedPublicKey(pPubKey: string): string {
    let keyLines: string[] = [];
    if (pPubKey) {
        keyLines = pPubKey.split('\n');
        keyLines.shift(); // Remove the "BEGIN" first line
        while (keyLines.length > 1
                && ( keyLines[keyLines.length-1].length < 1 || keyLines[keyLines.length-1].includes('END PUBLIC KEY') ) ) {
            keyLines.pop();   // Remove the "END" last line
        };
    }
    return keyLines.join('');    // Combine all lines into one long string
};

// Process the user request data and update fields fields in the account.
// This is infomation from the user from heartbeats or location updates.
// Return a VKeyedCollection of the updates made so it can be used to update the database.
export async function updateLocationInfo(pReq: Request): Promise<VKeyedCollection> {
    let newLoc: VKeyedCollection = {};
    if (pReq.vAuthAccount && pReq.body.location) {
        try {
            // build location from what is in the account already
            const loc = pReq.body.location;
            for (const field of ['connected', 'path', 'place_id', 'domain_id',
                            'network_address', 'node_id', 'availability']) {
                if (loc.hasOwnProperty(field)) {
                    await Accounts.setField(pReq.vAuthToken, pReq.vAuthAccount, field, loc[field], pReq.vAuthAccount, newLoc);
                };
            };
        }
        catch ( err ) {
            Logger.error(`procPutUserLocation: exception parsing put from ${pReq.vAuthAccount.username}: ${err}`);
            pReq.vRestResp.respondFailure(`exception parsing request: ${err}`);
            newLoc = undefined;
        };
    };
    return newLoc;
};

// The returned location info has many options depending on whether
//    the account has set location and/or has an associated domain.
// Return a structure that represents the target account's domain
export async function buildLocationInfo(pAcct: AccountEntity): Promise<any> {
    let ret: any = {};
    if (pAcct.locationDomainId) {
        const aDomain = await Domains.getDomainWithId(pAcct.locationDomainId);
        if (IsNotNullOrEmpty(aDomain)) {
            ret = {
                'root': {
                    'domain': await buildDomainInfo(aDomain),
                },
                'path': pAcct.locationPath,
            };
        }
        else {
            // The domain doesn't have an ID
            ret = {
                'root': {
                    'domain': {
                        'network_address': pAcct.locationNetworkAddress,
                        'network_port': pAcct.locationNetworkPort
                    }
                }
            };
        };
    };
    ret.node_id = pAcct.locationNodeId;
    ret.online = Accounts.isOnline(pAcct)
    return ret;
};

// A smaller, top-level domain info block
export async function buildDomainInfo(pDomain: DomainEntity): Promise<any> {
    return {
        'id': pDomain.id,
        'domainId': pDomain.id,
        'name': pDomain.name,
        'visibility': pDomain.visibility ?? Visibility.OPEN,
        'capacity': pDomain.capacity,
        'sponsorAccountId': pDomain.sponsorAccountId,
        'label': pDomain.name,
        'network_address': pDomain.networkAddr,
        'network_port': pDomain.networkPort,
        'ice_server_address': pDomain.iceServerAddr,
        'version': pDomain.version,
        'protocol_version': pDomain.protocol,
        'active': pDomain.active ?? false,
        'time_of_last_heartbeat': pDomain.timeOfLastHeartbeat?.toISOString(),
        'time_of_last_heartbeat_s': pDomain.timeOfLastHeartbeat?.getTime().toString(),
        'num_users': pDomain.numUsers
    };
};

// Return a structure with the usual domain information.
export async function buildDomainInfoV1(pDomain: DomainEntity): Promise<any> {
    return {
        'domainId': pDomain.id,
        'id': pDomain.id,       // legacy
        'name': pDomain.name,
        'visibility': pDomain.visibility ?? Visibility.OPEN,
        'world_name': pDomain.name,   // legacy
        'label': pDomain.name,        // legacy
        'public_key': pDomain.publicKey ? createSimplifiedPublicKey(pDomain.publicKey) : undefined,
        'owner_places': await buildPlacesForDomain(pDomain),
        'sponsor_account_id': pDomain.sponsorAccountId,
        'ice_server_address': pDomain.iceServerAddr,
        'version': pDomain.version,
        'protocol_version': pDomain.protocol,
        'network_address': pDomain.networkAddr,
        'network_port': pDomain.networkPort,
        'automatic_networking': pDomain.networkingMode,
        'restricted': pDomain.restricted,
        'num_users': pDomain.numUsers,
        'anon_users': pDomain.anonUsers,
        'total_users': pDomain.numUsers,
        'capacity': pDomain.capacity,
        'description': pDomain.description,
        'maturity': pDomain.maturity ?? Maturity.UNRATED,
        'restriction': pDomain.restriction,
        'managers': pDomain.managers,
        'tags': pDomain.tags,
        'meta': {
            'capacity': pDomain.capacity,
            'contact_info': pDomain.contactInfo,
            'description': pDomain.description,
            'images': pDomain.images,
            'managers': pDomain.managers,
            'restriction': pDomain.restriction,
            'tags': pDomain.tags,
            'thumbnail': pDomain.thumbnail,
            'world_name': pDomain.name
        },
        'users': {
            'num_anon_users': pDomain.anonUsers,
            'num_users': pDomain.numUsers,
            'user_hostnames': pDomain.hostnames
        },
        'time_of_last_heartbeat': pDomain.timeOfLastHeartbeat?.toISOString(),
        'time_of_last_heartbeat_s': pDomain.timeOfLastHeartbeat?.getTime().toString(),
        'last_sender_key': pDomain.lastSenderKey,
        'addr_of_first_contact': pDomain.iPAddrOfFirstContact,
        'when_domain_entry_created': pDomain.whenCreated?.toISOString(),
        'when_domain_entry_created_s': pDomain.whenCreated?.getTime().toString()
    };
};

// Return the limited "user" info.. used by /api/v1/users
export async function buildUserInfo(pAccount: AccountEntity): Promise<any> {
    return {
        'accountId': pAccount.id,
        'id': pAccount.id,
        'username': pAccount.username,
        'images': await buildImageInfo(pAccount),
        'location': await buildLocationInfo(pAccount),
    };
};

export async function buildImageInfo(pAccount: AccountEntity): Promise<any> {
    const ret: VKeyedCollection = {};
    if (pAccount.imagesTiny) ret.tiny = pAccount.imagesTiny;
    if (pAccount.imagesHero) ret.hero = pAccount.imagesHero;
    if (pAccount.imagesThumbnail) ret.thumbnail = pAccount.imagesThumbnail;
    return ret;
};

// Return the block of account information.
// Used by several of the requests to return the complete account information.
export async function buildAccountInfo(pReq: Request, pAccount: AccountEntity): Promise<any> {
    return {
        'accountId': pAccount.id,
        'id': pAccount.id,
        'username': pAccount.username,
        'email': pAccount.email,
        'administrator': Accounts.isAdmin(pAccount),
        'enabled': Accounts.isEnabled(pAccount),
        'roles': pAccount.roles,
        'availability': pAccount.availability,
        'public_key': createSimplifiedPublicKey(pAccount.sessionPublicKey),
        'images': {
            'hero': pAccount.imagesHero,
            'tiny': pAccount.imagesTiny,
            'thumbnail': pAccount.imagesThumbnail
        },
        'profile_detail': pAccount.profileDetail,
        'location': await buildLocationInfo(pAccount),
        'friends': pAccount.friends,
        'connections': pAccount.connections,
        'when_account_created': pAccount.whenCreated?.toISOString(),
        'when_account_created_s': pAccount.whenCreated?.getTime().toString(),
        'time_of_last_heartbeat': pAccount.timeOfLastHeartbeat?.toISOString(),
        'time_of_last_heartbeat_s': pAccount.timeOfLastHeartbeat?.getTime().toString(),
    };
};
// Return the block of account information used as the account 'profile'.
// Anyone can fetch a profile (if 'availability' is 'any') so not all info is returned
export async function buildAccountProfile(pReq: Request, pAccount: AccountEntity): Promise<any> {
    return {
        'accountId': pAccount.id,
        'id': pAccount.id,
        'username': pAccount.username,
        'images': {
            'hero': pAccount.imagesHero,
            'tiny': pAccount.imagesTiny,
            'thumbnail': pAccount.imagesThumbnail
        },
        'profile_detail': pAccount.profileDetail,
        'location': await buildLocationInfo(pAccount),
        'when_account_created': pAccount.whenCreated?.toISOString(),
        'when_account_created_s': pAccount.whenCreated?.getTime().toString(),
        'time_of_last_heartbeat': pAccount.timeOfLastHeartbeat?.toISOString(),
        'time_of_last_heartbeat_s': pAccount.timeOfLastHeartbeat?.getTime().toString(),
    };
};

// Return an object with the formatted place information
// Pass the PlaceEntity and the place's domain if known.
export async function buildPlaceInfo(pPlace: PlaceEntity, pDomain?: DomainEntity): Promise<any> {
    const ret = await buildPlaceInfoSmall(pPlace, pDomain);

    // if the place points to a domain, add that information also
    if (IsNotNullOrEmpty(pPlace.domainId)) {
        const aDomain = pDomain ?? await Domains.getDomainWithId(pPlace.domainId);
        if (aDomain) {
            ret.domain = await buildDomainInfo(aDomain);
        };
    };
    return ret;
};
// Return the basic information block for a Place
export async function buildPlaceInfoSmall(pPlace: PlaceEntity, pDomain?: DomainEntity): Promise<any> {
    const ret: VKeyedCollection =  {
        'placeId': pPlace.id,
        'id': pPlace.id,
        'name': pPlace.name,
        'displayName': pPlace.displayName,
        'visibility': pPlace.visibility ?? Visibility.OPEN,
        'address': await Places.getAddressString(pPlace),
        'path': pPlace.path,
        'description': pPlace.description,
        'maturity': pPlace.maturity ?? Maturity.UNRATED,
        'tags': pPlace.tags,
        'managers': await Places.getManagers(pPlace),
        'thumbnail': pPlace.thumbnail,
        'images': pPlace.images,
        'current_attendance': pPlace.currentAttendance ?? 0,
        'current_images': pPlace.currentImages,
        'current_info': pPlace.currentInfo,
        'current_last_update_time': pPlace.currentLastUpdateTime?.toISOString(),
        'current_last_update_time_s': pPlace.currentLastUpdateTime?.getTime().toString(),
        'last_activity_update': pPlace.lastActivity?.toISOString(),
        'last_activity_update_s': pPlace.lastActivity?.getTime().toString()
    };
    return ret;
};

// Return an array of Places names that are associated with the passed domain
export async function buildPlacesForDomain(pDomain: DomainEntity): Promise<any[]> {
    const ret: any[] = [];
    for await (const aPlace of Places.enumerateAsync(new GenericFilter({ 'domainId': pDomain.id }))) {
        ret.push(await buildPlaceInfoSmall(aPlace, pDomain));
    };
    return ret;
};