module Network.Messages exposing (..)

import Data exposing (..)
import Data.Info exposing (..)
import Data.Search
import Data.Schema exposing (Schema)
import Json.Decode as JD exposing (Decoder, Value)
import Json.Decode.Pipeline exposing (required)
import Json.Encode as JE
import Time exposing (Posix)
import Iso8601
import Data.Search exposing (SearchResponse)
import Notification exposing (Notification)

optional : (a -> Value) -> Maybe a -> Value
optional encoder value =
    case value of
        Just val -> encoder val
        Nothing -> JE.null

type SendMessage
    = InfoRequest
    | DatabaseAddRequest String String
    | DataInterviewAddRequest String
    | DataInterviewListPersonRequest Id
    | DataPersonAddRequest DataPersonAddRequestData
    | DataInterviewGet Id
    | DataPersonGet Id
    | FilePersonFetch FileFetchData
    | FileFetch Id
    | FileRename Id String
    | FileDelete Id
    | FileChangeConfidential Id Bool
    | FileMove Id (Maybe Id)
    | EditInterviewSend EditInterviewData
    | EditPersonSend EditPersonData
    | EditAttributeSend EditAttributeData
    | PerformReindex String
    | SearchSend SearchRequest
    | SearchCancel SearchRequest
    | SchemaRequest String
    | AccountRegister String (Maybe String) String String
    | AccountLogin String String
    | FidoReceive String JE.Value
    | DatabaseAddFinish String
    | DatabaseConnect (Maybe String)
    | DatabaseCancelCreation String
    | DatabaseUnlock String (Maybe String)
    | DatabaseLock String
    | DatabaseRemoveUser String String

encodeSendMessage : SendMessage -> Value
encodeSendMessage message =
    let
        enc : String -> List (String, Value) -> Value
        enc type_ items =
            JE.object
                <| (::) ("$type", JE.string type_)
                <| items
    in case message of
        InfoRequest -> 
            enc "InfoRequest" []
        DatabaseAddRequest name schema ->
            enc "DatabaseAddRequest"
            [ ("database", JE.string name)
            , ("schema", JE.string schema)
            ]
        DataInterviewAddRequest database -> 
            enc "DataInterviewAddRequest"
            [ ("database", JE.string database) ]
        DataInterviewListPersonRequest id ->
            enc "DataInterviewListPersonRequest"
            [ ("id", encodeId id) ]
        DataPersonAddRequest data ->
            enc "DataPersonAddRequest"
            <| encodeDataPersonAddRequestData data
        DataInterviewGet id ->
            enc "DataInterviewGet"
            [ ("id", encodeId id) ]
        DataPersonGet id ->
            enc "DataPersonGet"
            [ ("id", encodeId id) ]
        FilePersonFetch data ->
            enc "FilePersonFetch"
            <| encodeFileFetchData data
        FileFetch id ->
            enc "FileFetch" 
            [ ("id", encodeId id) ]
        FileRename id name ->
            enc "FileRename"
            [ ("id", encodeId id)
            , ("name", JE.string name)
            ]
        FileDelete id ->
            enc "FileDelete"
            [ ("id", encodeId id) ]
        FileChangeConfidential id conf ->
            enc "FileChangeConfidential"
            [ ("id", encodeId id)
            , ("confidential", JE.bool conf)
            ]
        FileMove id parent ->
            enc "FileMove"
            [ ("id", encodeId id)
            , ("parent", optional encodeId parent)
            ]
        EditInterviewSend data ->
            enc "EditInterviewSend"
            <| encodeEditInterviewData data
        EditPersonSend data ->
            enc "EditPersonSend"
            <| encodeEditPersonData data
        EditAttributeSend data ->
            enc "EditAttributeSend"
            <| encodeEditAttributeData data
        PerformReindex db ->
            enc "PerformReindex"
            [ ("database", JE.string db) ]
        SearchSend data ->
            enc "SearchSend"
            <| encodeSearchRequest data
        SearchCancel data ->
            enc "SearchCancel"
            <| encodeSearchRequest data
        SchemaRequest name ->
            enc "SchemaRequest"
            [ ("name", JE.string name) ]
        AccountRegister database key username password ->
            enc "AccountRegister"
            [ ("database", JE.string database)
            , ("key", optional JE.string key)
            , ("username", JE.string username)
            , ("password", JE.string password)
            ]
        AccountLogin username password ->
            enc "AccountLogin"
            [ ("username", JE.string username)
            , ("password", JE.string password)
            ]
        FidoReceive method value ->
            enc "FidoReceive"
            [ ("method", JE.string method)
            , ("value", value)
            ]
        DatabaseAddFinish id ->
            enc "DatabaseAddFinish"
            [ ("id", JE.string id) ]
        DatabaseConnect name ->
            enc "DatabaseConnect"
            [ ("name", optional JE.string name) ]
        DatabaseCancelCreation key ->
            enc "DatabaseCancelCreation"
            [ ("key", JE.string key) ]
        DatabaseUnlock database masterKey ->
            enc "DatabaseUnlock"
            [ ("database", JE.string database)
            , ("master-key", optional JE.string masterKey)
            ]
        DatabaseLock database ->
            enc "DatabaseLock"
            [ ("database", JE.string database) ]
        DatabaseRemoveUser database userId ->
            enc "DatabaseRemoveUser"
            [ ("database", JE.string database)
            , ("user-id", JE.string userId)
            ]

type ReceiveMessage
    = InfoSend Info
    | DatabaseAddRequestInfo NewDBInfo
    | DataInterviewAddSend Id
    | DataInterviewListPersonSend Id (List ListPersonInfo)
    | DataPersonAddSend Id
    | DataInterviewSend Id (Maybe Interview)
    | DataPersonSend Id (Maybe Person)
    | FilePersonSend FileFetchData (List File)
    | FileSend Id (Maybe File)
    | FileUpdated File
    | FileMoved File
    | EditAccept EditAcceptData
    | EditDenied EditDeniedData
    | SearchResponse Data.Search.SearchResponse
    | SchemaResponse String Schema
    | SendNotification Notification
    | AccountFidoRequest String (Maybe String) JE.Value
    | FidoResult Bool (Maybe String)
    | DatabaseCreated String
    | LoginRequest (Maybe String)

decodeReceiveMessage : Decoder ReceiveMessage
decodeReceiveMessage =
    JD.andThen
        (\type_ ->
            case type_ of
                "InfoSend" -> 
                    JD.map InfoSend decodeInfo
                "DatabaseAddRequestInfo" ->
                    JD.field "info" decodeNewDBInfo
                    |> JD.map DatabaseAddRequestInfo
                "DataInterviewAddSend" -> 
                    JD.succeed DataInterviewAddSend
                    |> required "id" decodeId
                "DataInterviewListPersonSend" ->
                    JD.succeed DataInterviewListPersonSend
                    |> required "id" decodeId
                    |> required "persons" (JD.list decodeListPersonInfo)
                "DataPersonAddSend" ->
                    JD.succeed DataPersonAddSend
                    |> required "id" decodeId
                "DataInterviewSend" ->
                    JD.succeed DataInterviewSend
                    |> required "id" decodeId
                    |> required "data" (JD.nullable decodeInterview)
                "DataPersonSend" ->
                    JD.succeed DataPersonSend
                    |> required "id" decodeId
                    |> required "data" (JD.nullable decodePerson)
                "FilePersonSend" ->
                    JD.map2 FilePersonSend
                        decodeFileFetchData
                    <| JD.field "data"
                    <| JD.list decodeFile
                "FileSend" ->
                    JD.succeed FileSend
                    |> required "id" decodeId
                    |> required "data" (JD.nullable decodeFile)
                "FileUpdated" ->
                    JD.map FileUpdated decodeFile
                "FileMoved" ->
                    JD.map FileMoved decodeFile
                "EditAccept" ->
                    JD.map EditAccept decodeEditAcceptData
                "EditDenied" ->
                    JD.map EditDenied decodeEditDeniedData
                "SearchResponse" ->
                    JD.map SearchResponse Data.Search.decodeSearchResponse
                "SchemaResponse" ->
                    JD.succeed SchemaResponse
                    |> required "name" JD.string
                    |> required "schema" Data.Schema.decode
                "SendNotification" ->
                    JD.succeed Notification
                    |> required "id" (JD.nullable JD.string)
                    |> required "img" (JD.nullable JD.string)
                    |> required "title" JD.string
                    |> required "description" JD.string
                    |> required "progress"
                        (JD.andThen
                            (\tag ->
                                case tag of
                                    "none" -> JD.succeed Notification.NoProgress
                                    "undefined" -> JD.succeed Notification.UndefinedProgress
                                    "progress" -> JD.map Notification.Progress
                                        <| JD.index 1 JD.float
                                    _ -> JD.fail <| "Unknown notification progress tag: " ++ tag
                            )
                        <| JD.index 0 JD.string
                        )
                    |> required "close" (JD.nullable Iso8601.decoder)
                    |> JD.map SendNotification
                "AccountFidoRequest" ->
                    JD.succeed AccountFidoRequest
                    |> required "command" JD.string
                    |> required "database" (JD.nullable JD.string)
                    |> required "value" JD.value
                "FidoResult" ->
                    JD.succeed FidoResult
                    |> required "is-success" JD.bool
                    |> required "error" (JD.nullable JD.string)
                "DatabaseCreated" ->
                    JD.succeed DatabaseCreated
                    |> required "id" JD.string
                "LoginRequest" ->
                    JD.succeed LoginRequest
                    |> required "database" (JD.nullable JD.string)
                _ -> JD.fail <| "Invalid type " ++ type_
        )
    <| JD.field "$type" JD.string

type alias DataPersonAddRequestData =
    { database: String
    , interview: String
    , role: String
    }

encodeDataPersonAddRequestData : DataPersonAddRequestData -> List (String, Value)
encodeDataPersonAddRequestData data =
    [ ("database", JE.string data.database)
    , ("interview", JE.string data.interview)
    , ("role", JE.string data.role)
    ]

type alias FileFetchData =
    { person: Id
    , root: Maybe Id
    }

decodeFileFetchData : Decoder FileFetchData
decodeFileFetchData =
    JD.succeed FileFetchData
        |> required "person" decodeId
        |> required "root" (JD.nullable decodeId)

encodeFileFetchData : FileFetchData -> List (String, Value)
encodeFileFetchData data =
    [ ("person", encodeId data.person)
    , ("root", optional encodeId data.root)
    ]

type alias EditInterviewData =
    { id: Id
    , delete: Bool
    , time: Maybe Posix
    , location: Maybe String
    , interviewer: Maybe String
    }

encodeEditInterviewData : EditInterviewData -> List (String, Value)
encodeEditInterviewData data =
    [ ("id", encodeId data.id)
    , ("delete", JE.bool data.delete)
    , ("time", optional Iso8601.encode data.time)
    , ("location", optional JE.string data.location)
    , ("interviewer", optional JE.string data.interviewer)
    ]

type alias EditPersonData =
    { id: Id
    , delete: Bool
    , name: Maybe String
    , contact: Maybe String
    }

encodeEditPersonData : EditPersonData -> List (String, Value)
encodeEditPersonData data =
    [ ("id", encodeId data.id)
    , ("delete", JE.bool data.delete)
    , ("name", optional JE.string data.name)
    , ("contact", optional JE.string data.contact)
    ]

type alias EditAttributeData =
    { id: Id
    , key: String
    , value: Value
    }

encodeEditAttributeData : EditAttributeData -> List (String, Value)
encodeEditAttributeData data =
    [ ("id", encodeId data.id)
    , ("key", JE.string data.key)
    , ("value", data.value)
    ]

type alias EditAcceptData =
    { id: Id
    , key: Maybe String
    , target: EditTarget
    }

decodeEditAcceptData : Decoder EditAcceptData
decodeEditAcceptData =
    JD.succeed EditAcceptData
        |> required "id" decodeId
        |> required "key" (JD.nullable JD.string)
        |> required "target" decodeEditTarget

type alias EditDeniedData =
    { id: Id
    , key: Maybe String
    , target: EditTarget
    , reasonCode: EditDeniedReason
    , reason: String
    }

decodeEditDeniedData : Decoder EditDeniedData
decodeEditDeniedData =
    JD.succeed EditDeniedData
        |> required "id" decodeId
        |> required "key" (JD.nullable JD.string)
        |> required "target" decodeEditTarget
        |> required "reasonCode" decodeEditDeniedReason
        |> required "reason" JD.string

type EditTarget
    = EditTargetInterview
    | EditTargetPerson
    | EditTargetAttribute

decodeEditTarget : Decoder EditTarget
decodeEditTarget =
    JD.andThen
        (\tar ->
            case tar of
                "Interview" -> JD.succeed EditTargetInterview
                "Person" -> JD.succeed EditTargetPerson
                "Attribute" -> JD.succeed EditTargetAttribute
                _ -> JD.fail <| "Unsupported Target " ++ tar
        )
        JD.string

type EditDeniedReason
    = EditDeniedIdNotFound
    | EditDeniedSchemaNotFound
    | EditDeniedAttributeNotAvailable
    | EditDeniedAttributeForbidden
    | EditDeniedInvalidValueType
    | EditDeniedGeneric

decodeEditDeniedReason : Decoder EditDeniedReason
decodeEditDeniedReason =
    JD.andThen
        (\reason ->
            case reason of
                "IdNotFound" -> JD.succeed EditDeniedIdNotFound
                "SchemaNotFound" -> JD.succeed EditDeniedSchemaNotFound
                "AttributeNotAvailable" -> JD.succeed EditDeniedAttributeNotAvailable
                "AttributeForbidden" -> JD.succeed EditDeniedAttributeForbidden
                "InvalidValueType" -> JD.succeed EditDeniedInvalidValueType
                "Generic" -> JD.succeed EditDeniedGeneric
                _ -> JD.fail <| "Unsupported Reason " ++ reason
        )
        JD.string

type alias SearchRequest =
    { query: String
    , uiId: String
    }

encodeSearchRequest : SearchRequest -> List (String, Value)
encodeSearchRequest request =
    [ ("query", JE.string request.query)
    , ("uiId", JE.string request.uiId)
    ]

type alias ListPersonInfo =
    { id: Id
    , role: String
    }

decodeListPersonInfo : Decoder ListPersonInfo
decodeListPersonInfo =
    JD.succeed ListPersonInfo
    |> required "id" decodeId
    |> required "role" JD.string

type alias NewDBInfo =
    { key: String
    , database: String
    , schema: String
    , access: List Data.Info.DbAccess
    }

decodeNewDBInfo : Decoder NewDBInfo
decodeNewDBInfo =
    JD.succeed NewDBInfo
    |> required "key" JD.string
    |> required "database" JD.string
    |> required "schema" JD.string
    |> required "access"
        (JD.list Data.Info.decodeDbAccess)
