# -*- encoding: utf-8 -*-
# frozen_string_literal: true

class Pplcdid
    # log functions -----------------------------
    def self.add_hash(log)
        log.map do |item|
            i = item.dup
            i.delete("previous")
            item["entry-hash"] = hash(canonical(item))
            if item.transform_keys(&:to_s)["op"] == 1
                item["sub-entry-hash"] = hash(canonical(i))
            end
            item
        end
    end

    # check if signature matches current document
    # check if signature in log is correct
    def self.match_log_did?(log, doc)
        message = log["doc"].to_s
        signature = log["sig"].to_s
        public_keys = doc["key"].to_s
        public_key = public_keys.split(":")[0] rescue ""
        return verify(message, signature, public_key).first
    end

    def self.retrieve_log(did_hash, log_file, log_location, options)
        if log_location == ""
            log_location = DEFAULT_LOCATION
        end
        if !(log_location == "" || log_location == "local")
            if !log_location.start_with?("http")
                log_location = "https://" + log_location
            end
        end

        case log_location
        when /^http/
            log_location = log_location.sub("%3A%2F%2F","://")
            retVal = HTTParty.get(log_location + "/log/" + did_hash)
            if retVal.code != 200
                msg = retVal.parsed_response("error").to_s rescue 
                        "invalid response from " + log_location.to_s + "/log/" + did_hash.to_s

                return [nil, msg]
            end
            if options.transform_keys(&:to_s)["trace"]
                if options[:silent].nil? || !options[:silent]
                    puts "GET log for " + did_hash + " from " + log_location
                end
            end
            retVal = JSON.parse(retVal.to_s) rescue nil
            return [retVal, ""]
        when "", "local"
            doc = JSON.parse(read_private_storage(log_file)) rescue {}
            if doc == {}
                return [nil, "cannot read file '" + log_file + "'"]
            end
            return [doc, ""]
        end
    end

    def self.dag_did(logs, options)
        dag = DAG.new
        dag_log = []
        log_hash = []
        
        # calculate hash values for each entry and build vertices
        i = 0
        create_entries = 0
        create_index = nil
        terminate_indices = []
        logs.each do |el|
            if el["op"].to_i == 2
                create_entries += 1
                create_index = i
            end
            if el["op"].to_i == 0
                terminate_indices << i
            end
            log_hash << pplcdid.hash(pplcdid.canonical(el))
            dag_log << dag.add_vertex(id: i)
            i += 1
        end unless logs.nil?

        if create_entries != 1
            return [nil, nil, nil, "wrong number of CREATE entries (" + create_entries.to_s + ") in log" ]
        end
        if terminate_indices.length == 0
            return [nil, nil, nil, "missing TERMINATE entries" ]
        end 

        # create edges between vertices
        i = 0
        logs.each do |el|
            el["previous"].each do |p|
                position = log_hash.find_index(p)
                if !position.nil?
                    dag.add_edge from: dag_log[position], to: dag_log[i]
                end
            end unless el["previous"] == []
            i += 1
        end unless logs.nil?

        # identify tangling TERMINATE entry
        i = 0
        terminate_entries = 0
        terminate_overall = 0
        terminate_index = nil
        logs.each do |el|
            if el["op"].to_i == 0
                if dag.vertices[i].successors.length == 0
                    terminate_entries += 1
                    terminate_index = i
                end
                terminate_overall += 1
            end
            i += 1
        end unless logs.nil?

        if terminate_entries != 1 && !options[:log_complete]
            if options[:silent].nil? || !options[:silent]
                return [nil, nil, nil, "cannot resolve DID" ]
            end
        end 
        return [dag, create_index, terminate_index, ""]
    end

    def self.dag2array(dag, log_array, index, result, options)
        if options.transform_keys(&:to_s)["trace"]
            if options[:silent].nil? || !options[:silent]
                puts "    vertex " + index.to_s + " at " + log_array[index]["ts"].to_s + " op: " + log_array[index]["op"].to_s + " doc: " + log_array[index]["doc"].to_s
            end
        end
        result << log_array[index]
        dag.vertices[index].successors.each do |s|
            # check if successor has predecessor that is not self (i.e. REVOKE with TERMINATE)
            s.predecessors.each do |p|
                if p[:id] != index
                    if options.transform_keys(&:to_s)["trace"]
                        if options[:silent].nil? || !options[:silent]
                            puts "    vertex " + p[:id].to_s + " at " + log_array[p[:id]]["ts"].to_s + " op: " + log_array[p[:id]]["op"].to_s + " doc: " + log_array[p[:id]]["doc"].to_s
                        end
                    end
                    result << log_array[p[:id]]
                end
            end unless s.predecessors.length < 2
            dag2array(dag, log_array, s[:id], result, options)
        end unless dag.vertices[index].successors.count == 0
        result
    end

    def self.dag_update(currentDID, options)
        i = 0
        doc_location = ""
        initial_did = currentDID["did"].to_s.dup
        initial_did = initial_did.delete_prefix("did:pplc:")
        if initial_did.include?(LOCATION_PREFIX)
            tmp = initial_did.split(LOCATION_PREFIX)
            initial_did = tmp[0] 
            doc_location = tmp[1]
        end
        current_public_doc_key = ""
        verification_output = false
        currentDID["log"].each do |el|
            case el["op"]
            when 2,3 # CREATE, UPDATE
                currentDID["doc_log_id"] = i
                doc_did = el["doc"]
                did_hash = doc_did.delete_prefix("did:pplc:")
                did_hash = did_hash.split(LOCATION_PREFIX).first
                did10 = did_hash[0,10]
                doc = retrieve_document_raw(doc_did, did10 + ".doc", doc_location, {})
                if doc.first.nil?
                    currentDID["error"] = 2
                    msg = doc.last.to_s
                    if msg == ""
                        msg = "cannot retrieve " + doc_did.to_s
                    end
                    currentDID["message"] = msg
                    return currentDID
                end
                doc = doc.first["doc"]
                if el["op"] == 2 # CREATE
                    if !match_log_did?(el, doc)
                        currentDID["error"] = 1
                        currentDID["message"] = "Signatures in log don't match"
                        return currentDID
                    end
                end
                currentDID["did"] = doc_did
                currentDID["doc"] = doc
                # since hash is guaranteed during retrieve_document this check is not necessary
                # if hash(canonical(doc)) != did_hash
                #     currentDID["error"] = 1
                #     currentDID["message"] = "DID identifier and DID document don't match"
                #     if did_hash == initial_did
                #         verification_output = true
                #     end
                #     if verification_output
                #         currentDID["verification"] += "identifier: " + did_hash.to_s + "\n"
                #         currentDID["verification"] += "⛔ does not match DID Document:" + "\n"
                #         currentDID["verification"] += JSON.pretty_generate(doc) + "\n"
                #         currentDID["verification"] += "(Details: https://peoplecarbon.github.io/pplcdid/#calculate_hash)" + "\n\n"
                #     end
                #     return currentDID
                # end
                if did_hash == initial_did
                    verification_output = true
                end
                if verification_output
                    currentDID["verification"] += "identifier: " + did_hash.to_s + "\n"
                    currentDID["verification"] += "✅ is hash of DID Document:" + "\n"
                    currentDID["verification"] += JSON.pretty_generate(doc) + "\n"
                    currentDID["verification"] += "(Details: https://peoplecarbon.github.io/pplcdid/#calculate_hash)" + "\n\n"
                end
                current_public_doc_key = currentDID["doc"]["key"].split(":").first rescue ""

            when 0 # TERMINATE
                currentDID["termination_log_id"] = i

                doc_did = currentDID["did"]
                did_hash = doc_did.delete_prefix("did:pplc:")
                did_hash = did_hash.split(LOCATION_PREFIX).first
                did10 = did_hash[0,10]
                doc = retrieve_document_raw(doc_did, did10 + ".doc", doc_location, {})
                # since it retrieves a DID that previously existed, this test is not necessary
                # if doc.first.nil?
                #     currentDID["error"] = 2
                #     currentDID["message"] = doc.last.to_s
                #     return currentDID
                # end
                doc = doc.first["doc"]
                term = doc["log"]
                log_location = term.split(LOCATION_PREFIX)[1] rescue ""
                if log_location.to_s == ""
                    log_location = DEFAULT_LOCATION
                end
                term = term.split(LOCATION_PREFIX).first
                if hash(canonical(el)) != term
                    currentDID["error"] = 1
                    currentDID["message"] = "Log reference and record don't match"
                    if verification_output
                        currentDID["verification"] += "'log' reference in DID Document: " + term.to_s + "\n"
                        currentDID["verification"] += "⛔ does not match TERMINATE log record:" + "\n"
                        currentDID["verification"] += JSON.pretty_generate(el) + "\n"
                        currentDID["verification"] += "(Details: https://peoplecarbon.github.io/pplcdid/#calculate_hash)" + "\n\n"
                    end
                    return currentDID
                end
                if verification_output
                    currentDID["verification"] += "'log' reference in DID Document: " + term.to_s + "\n"
                    currentDID["verification"] += "✅ is hash of TERMINATE log record:" + "\n"
                    currentDID["verification"] += JSON.pretty_generate(el) + "\n"
                    currentDID["verification"] += "(Details: https://peoplecarbon.github.io/pplcdid/#calculate_hash)" + "\n\n"
                end

                # check if there is a revocation entry
                revocation_record = {}
                revoc_term = el["doc"]
                revoc_term = revoc_term.split(LOCATION_PREFIX).first
                revoc_term_found = false
                log_array, msg = retrieve_log(did_hash, did10 + ".log", log_location, options)
                log_array.each do |log_el|
                    log_el_structure = log_el.dup
                    if log_el["op"].to_i == 1 # TERMINATE
                        log_el_structure.delete("previous")
                    end
                    if hash(canonical(log_el_structure)) == revoc_term
                        revoc_term_found = true
                        revocation_record = log_el.dup
                        if verification_output
                            currentDID["verification"] += "'doc' reference in TERMINATE log record: " + revoc_term.to_s + "\n"
                            currentDID["verification"] += "✅ is hash of REVOCATION log record (without 'previous' attribute):" + "\n"
                            currentDID["verification"] += JSON.pretty_generate(log_el) + "\n"
                            currentDID["verification"] += "(Details: https://peoplecarbon.github.io/pplcdid/#calculate_hash)" + "\n\n"
                        end
                        break
                    end
                end unless log_array.nil?
                # this should actually be covered by retrieve_log in the block above
                # (actually I wasn't able to craft a test case covering this part...)
                # if !options.transform_keys(&:to_s)["log_location"].nil?
                #     log_array, msg = retrieve_log(revoc_term, did10 + ".log", options.transform_keys(&:to_s)["log_location"], options)
                #     log_array.each do |log_el|
                #         if log_el["op"] == 1 # TERMINATE
                #             log_el_structure = log_el.delete("previous")
                #         else
                #             log_el_structure = log_el
                #         end
                #         if hash(canonical(log_el_structure)) == revoc_term
                #             revoc_term_found = true
                #             revocation_record = log_el.dup
                #             if verification_output
                #                 currentDID["verification"] += "'doc' reference in TERMINATE log record: " + revoc_term.to_s + "\n"
                #                 currentDID["verification"] += "✅ is hash of REVOCATION log record (without 'previous' attribute):" + "\n"
                #                 currentDID["verification"] += JSON.pretty_generate(log_el) + "\n"
                #                 currentDID["verification"] += "(Details: https://peoplecarbon.github.io/pplcdid/#calculate_hash)" + "\n\n"
                #             end
                #             break
                #         end
                #     end
                # end

                if revoc_term_found
                    update_term_found = false
                    log_array.each do |log_el|
                        if log_el["op"].to_i == 3
                            if log_el["previous"].include?(hash(canonical(revocation_record)))
                                update_term_found = true
                                message = log_el["doc"].to_s

                                signature = log_el["sig"]
                                public_key = current_public_doc_key.to_s
                                signature_verification = verify(message, signature, public_key).first
                                if signature_verification
                                    if verification_output
                                        currentDID["verification"] += "found UPDATE log record:" + "\n"
                                        currentDID["verification"] += JSON.pretty_generate(log_el) + "\n"
                                        currentDID["verification"] += "✅ public key from last DID Document: " + current_public_doc_key.to_s + "\n"
                                        currentDID["verification"] += "verifies 'doc' reference of new DID Document: " + log_el["doc"].to_s + "\n"
                                        currentDID["verification"] += log_el["sig"].to_s + "\n"
                                        currentDID["verification"] += "of next DID Document (Details: https://peoplecarbon.github.io/pplcdid/#verify_signature)" + "\n"

                                        next_doc_did = log_el["doc"].to_s
                                        next_doc_location = doc_location
                                        next_did_hash = next_doc_did.delete_prefix("did:pplc:")
                                        next_did_hash = next_did_hash.split(LOCATION_PREFIX).first
                                        next_did10 = next_did_hash[0,10]
                                        next_doc = retrieve_document_raw(next_doc_did, next_did10 + ".doc", next_doc_location, {})
                                        if next_doc.first.nil?
                                            currentDID["error"] = 2
                                            currentDID["message"] = next_doc.last
                                            return currentDID
                                        end
                                        next_doc = next_doc.first["doc"]
                                        if public_key == next_doc["key"].split(":").first
                                            currentDID["verification"] += "⚠️  no key rotation in updated DID Document" + "\n"
                                        end
                                        currentDID["verification"] += "\n"
                                    end
                                else
                                    currentDID["error"] = 1
                                    currentDID["message"] = "Signature does not match"
                                    if verification_output
                                        new_doc_did = log_el["doc"].to_s
                                        new_doc_location = doc_location
                                        new_did_hash = new_doc_did.delete_prefix("did:pplc:")
                                        new_did_hash = new_did_hash.split(LOCATION_PREFIX).first
                                        new_did10 = new_did_hash[0,10]
                                        new_doc = retrieve_document(new_doc_did, new_did10 + ".doc", new_doc_location, {}).first
                                        currentDID["verification"] += "found UPDATE log record:" + "\n"
                                        currentDID["verification"] += JSON.pretty_generate(log_el) + "\n"
                                        currentDID["verification"] += "⛔ public key from last DID Document: " + current_public_doc_key.to_s + "\n"
                                        currentDID["verification"] += "does not verify 'doc' reference of new DID Document: " + log_el["doc"].to_s + "\n"
                                        currentDID["verification"] += log_el["sig"].to_s + "\n"
                                        currentDID["verification"] += "next DID Document (Details: https://peoplecarbon.github.io/pplcdid/#verify_signature)" + "\n"
                                        currentDID["verification"] += JSON.pretty_generate(new_doc) + "\n\n"
                                    end
                                    return currentDID
                                end
                                break
                            end
                        end
                    end

                else
                    if verification_output
                        currentDID["verification"] += "Revocation reference in log record: " + revoc_term.to_s + "\n"
                        currentDID["verification"] += "✅ cannot find revocation record searching at" + "\n"
                        currentDID["verification"] += "- " + log_location + "\n"
                        if !options.transform_keys(&:to_s)["log_location"].nil?
                            currentDID["verification"] += "- " + options.transform_keys(&:to_s)["log_location"].to_s + "\n"
                        end
                        currentDID["verification"] += "(Details: https://peoplecarbon.github.io/pplcdid/#retrieve_log)" + "\n\n"
                    end
                    break
                end
            when 1 # revocation log entry
                # do nothing
            else
                currentDID["error"] = 2
                currentDID["message"] = "FATAL ERROR: op code '" + el["op"].to_s + "' not implemented"
                return currentDID

            end
            i += 1
        end unless currentDID["log"].nil?

        return currentDID
    end

end