import Foundation
import WebSocketKit
import Vapor
import Fluent
import Clairvoyant

let maximumPlayersPerTable = 4

typealias TableId = String
typealias TableName = String

final class TableManagement {
    
    /// All tables indexed by their id
    private var tables = [UUID : ManageableTable]()

    /// The metric to log the current number of tables
    private var tableCountMetric: Metric<Int>

    /// The metric describing the number of players currently sitting at a table
    private let playingPlayerCountMetric: Metric<Int>

    /// The metric describing the number of players currently connected via a websocket
    private let connectedPlayerCountMetric: Metric<Int>
    
    var tableCount: Int {
        tables.count
    }

    /**
     Load the tables from a file in the storage folder
     - Throws: Errors when the file could not be read
     */
    init(database: Database) async throws {
        self.tableCountMetric = .init(
            "schafkopf.tables",
            name: "Open tables",
            description: "The number of currently available tables")
        self.playingPlayerCountMetric = .init(
            "schafkopf.playing",
            name: "Sitting players",
            description: "The number of players currently sitting at a table")
        self.connectedPlayerCountMetric = .init(
            "schafkopf.connected",
            name: "Connected players",
            description: "The number of players with a websocket connection to the server")


        do {
            try await loadTables(from: database)
        } catch {
            log("Failed to load tables: \(error)")
        }
    }

    private func loadTables(from database: Database) async throws {
        try await Table.query(on: database).with(\.$players).all().forEach { table in
            guard !table.players.isEmpty else {
                _ = table.delete(on: database)
                return
            }
            let id = table.id!
            self.tables[id] = WaitingTable(id: id, name: table.name, isPublic: table.isPublic, players: table.players)
        }
        await logTableCount()
        await logPlayingPlayerCount()
        await logConnectedPlayerCount()
    }

    private func logTableCount() async {
        _ = try? await tableCountMetric.update(tables.count)
    }

    private func logPlayingPlayerCount() async {
        let count = tables.values.sum { $0.playerCount }
        _ = try? await playingPlayerCountMetric.update(count)
    }

    private func logConnectedPlayerCount() async {
        let count = tables.values.sum { $0.numberOfConnectedPlayers }
        _ = try? await connectedPlayerCountMetric.update(count)
    }

    /**
     Create a new table with optional players.
     - Parameter name: The name of the table
     - Parameter players: The player creating the table
     - Parameter isPublic: Indicates that this is a game joinable by everyone
     - Returns: The table id
     */
    func createTable(named name: TableName, player: User, isPublic: Bool, in database: Database) async throws -> TableInfo {
        let table = Table(name: name, isPublic: isPublic)
        try await table.create(on: database)
        player.$table.id = table.id
        try await player.update(on: database)
        let waitingTable = WaitingTable(newTable: table, user: player)
        self.tables[waitingTable.id] = waitingTable
        await logTableCount()
        await logPlayingPlayerCount()
        return waitingTable.tableInfo(forPlayer: player.name)
    }
    
    /// A list of all public tables
    var publicTableList: [PublicTableInfo] {
        tables.values.filter { $0.isPublic }.map { $0.publicInfo }
    }

    /**
     Get the table info for a player
     - Parameter player: The name of the player
     - Returns: The table info, if the player has joined a table
     */
    func tableInfo(player: PlayerName) -> TableInfo? {
        currentTable(for: player)?.tableInfo(forPlayer: player)
    }

    private func currentTable(for player: PlayerName) -> ManageableTable? {
        tables.values.first(where: { $0.playerNames.contains(player) })
    }
    
    /**
     Join a table.
     - Parameter tableId: The table to join
     - Parameter player: The name of the player who wants to join.
     - Returns: The result of the join operation
     */
    func join(tableId: UUID, player: User, in database: Database) async throws -> TableInfo {
        let table = try joinableTable(for: player, id: tableId)
        player.$table.id = table.id
        try await player.update(on: database)
        table.sendUpdateToAllPlayers()
        await logPlayingPlayerCount()
        return table.tableInfo(forPlayer: player.name)
    }

    private func joinableTable(for player: User, id tableId: UUID) throws -> ManageableTable {
        if let existing = self.currentTable(for: player.name) {
            guard existing.id == tableId else {
                throw Abort(.forbidden) // 403
            }
            return existing
        }
        guard let table = self.tables[tableId] else {
            throw Abort(.gone) // 410
        }
        guard let joinableTable = table as? WaitingTable,
              joinableTable.add(player: player.name, points: player.points) else {
                  throw Abort(.expectationFailed) // 417
              }
        return joinableTable
    }
    
    /**
     A player leaves the table it previously joined
     - Parameter player: The player leaving the table
     */
    func leaveTable(player: User, in database: Database) async throws {
        guard let oldTable = currentTable(for: player.name) else {
            return
        }
        player.$table.id = nil
        guard let table = WaitingTable(oldTable: oldTable, removing: player.name) else {
            tables[oldTable.id] = nil
            await logTableCount()
            try await player.update(on: database)
            try await Table.query(on: database).filter(\.$id == oldTable.id).delete()
            return
        }
        /// `player.canStartGame` is automatically set to false, because table is not full
        tables[table.id] = table
        #warning("Update points for all players, add penalty if running game")
        table.sendUpdateToAllPlayers()
        await logPlayingPlayerCount()
        await logConnectedPlayerCount()
        try await player.update(on: database)
    }
    
    func connect(player: PlayerName, using socket: WebSocket) async -> Bool {
        guard let table = currentTable(for: player) else {
            return false
        }
        let result = table.connect(player: player, using: socket)
        await logConnectedPlayerCount()
        return result
    }
    
    func disconnect(player: PlayerName) async {
        guard let table = currentTable(for: player) else {
            return
        }
        table.disconnect(player: player)
        await logConnectedPlayerCount()
    }
    
    func performAction(player: PlayerName, action: PlayerAction) -> PlayerActionResult {
        guard let table = currentTable(for: player) else {
            log("Player \(player) wants to \(action.id), but no table joined")
            return .noTableJoined
        }
        let (result, newTable) = table.perform(action: action, forPlayer: player)
        guard result == .success else {
            return result
        }
        guard let newTable = newTable else {
            table.sendUpdateToAllPlayers()
            return .success
        }
        tables[newTable.id] = newTable
        newTable.sendUpdateToAllPlayers()
        if newTable is FinishedTable || newTable is DealingTable {
            // TODO: Save new table
        }
        return .success
    }

    func select(game: GameType, player: PlayerName) -> PlayerActionResult {
        guard let aTable = currentTable(for: player) else {
            log("Player \(player) wants to play \(game.rawValue), but no table joined")
            return .noTableJoined
        }
        guard let table = aTable as? BiddingTable else {
            return .tableStateInvalid
        }
        let (result, newTable) = table.select(game: game, player: player)
        guard result == .success else {
            return result
        }
        guard let newTable = newTable else {
            log("Game selected by \(player), but no playing table \(table.name) created")
            table.sendUpdateToAllPlayers()
            return result
        }
        tables[newTable.id] = newTable
        newTable.sendUpdateToAllPlayers()
        // TODO: Save new table
        return .success
    }

    func play(card: Card, player: PlayerName, in database: Database) async throws -> PlayerActionResult {
        guard let table = currentTable(for: player) else {
            return .noTableJoined
        }
        let (result, newTable) = table.play(card: card, player: player)
        guard result == .success else {
            return result
        }
        guard let newTable = newTable else {
            table.sendUpdateToAllPlayers()
            return .success
        }
        tables[newTable.id] = newTable
        if let finished = newTable as? FinishedTable {
            try await finished.updatePlayerPoints(in: database)
        }
        newTable.sendUpdateToAllPlayers()
        return .success
    }
    
    func disconnectAllSockets() async {
        tables.values.forEach { $0.disconnectAllPlayers() }
        await logConnectedPlayerCount()
    }
}
