package dropbox

import (
    "encoding/json"
    "encoding/hex"
    "strings"
    "io/ioutil"
    "time"
    "strconv"
    "net/url"
    "errors"
    "sort"
    "crypto/md5"

    "github.com/aniiyengar/dbpedia/api/utils"
    "github.com/sergi/go-diff/diffmatchpatch"
)

type PageShard struct {
    PageName string
    Editor string
    Edits []PageEdit
}

type PageEdit struct {
    PageName string     `json:"page_name"`
    Editor string       `json:"editor"`
    Timestamp string    `json:"timestamp"`
    PrevHash string     `json:"prev_hash"`

    Diff string         `json:"diff"`
}

type Page struct {
    Title string `json:"page_name"`
    Data string `json:"page_data"`
}

func AddPageToIndexIfNotExists(pageName string) error {
    pageName = strings.ToLower(pageName)

    indexData, err := GetIndex()
    if err != nil {
        return err
    }

    exists := false
    pageList := indexData.Pages
    for name, _ := range pageList {
        if name == pageName {
            exists = true
            break
        }
    }

    if exists {

        return nil
    } else {
        indexData.Pages[pageName] = []string{}
    }

    centralToken := utils.Config()["DROPBOX_CENTRAL_ACCOUNT_TOKEN"]

    jsonBytes, err := json.Marshal(indexData)
    reqJson := []byte(`{ "path": "/dbpedia_index.json", "mode": "overwrite", "mute": true }`)
    resp, err := utils.MakeRequest(
        "https:
        "POST",
        jsonBytes,
        map[string]string{
            "Content-Type": "application/octet-stream",
            "Dropbox-API-Arg": string(reqJson),
            "Authorization": "Bearer " + centralToken,
        },
    )
    if err != nil {
        return err
    } else {
        defer resp.Body.Close()
    }

    return nil
}

func GetPageShards(pageName string) ([]PageShard, error) {

    pageName = strings.ToLower(pageName)

    centralToken := utils.Config()["DROPBOX_CENTRAL_ACCOUNT_TOKEN"]
    indexData, err := GetIndex()
    if err != nil {
        return []PageShard{}, err
    }

    exists := false
    pageList := indexData.Pages
    for name, _ := range pageList {
        if strings.ToLower(name) == pageName {
            exists = true
            break
        }
    }

    if !exists {
        return []PageShard{}, nil
    }

    editors := indexData.Pages[pageName]
    var requestsToMake []utils.Request

    utils.Debug(pageName)

    for _, editor := range editors {

        fileName := "/dbpedia_data:" + editor + "/" + pageName
        newReq := utils.Request{
            Url: "https:
            Method: "POST",
            Data: []byte{},
            Headers: map[string]string{
                "Authorization": "Bearer " + centralToken,
                "Dropbox-API-Arg": "{ \"path\": \"" + fileName + "\" }",
            },
        }
        requestsToMake = append(requestsToMake, newReq)
    }

    var reqResults []utils.ParallelRequestResponse
    reqResults = utils.MakeParallelRequests(requestsToMake, 8) 

    var returnValue []PageShard
    for _, res := range reqResults {
        if res.Error != nil {
            return []PageShard{}, res.Error
        } else {
            defer res.Response.Body.Close()
        }

        resData, err := ioutil.ReadAll(res.Response.Body)
        if err != nil {
            return []PageShard{}, err
        }

        edits, err := BytesToPageEdits(resData)
        if err != nil {
            return []PageShard{}, err
        }

        pageShard := PageShard{
            PageName: pageName,
            Editor: editors[res.RequestIndex],
            Edits: edits,
        }
        returnValue = append(returnValue, pageShard)
    }

    return returnValue, nil
}

type pageEditSerialization struct {
    edits []PageEdit
}

func PageEditsToBytes(edits []PageEdit) ([]byte, error) {
    var resultString string

    for _, edit := range edits {
        bts, err := PageEditToBytes(edit)
        if err != nil {
            return nil, err
        }

        resultString = resultString + string(bts)
    }

    return []byte(resultString), nil
}

func BytesToPageEdits(data []byte) ([]PageEdit, error) {
    dataString := string(data)
    editStrings := strings.Split(dataString, "\n")
    var result []PageEdit

    for _, editString := range editStrings {
        if len(editString) != 0 {

            unescaped, err := url.QueryUnescape(editString)
            if err != nil {
                return []PageEdit{}, err
            }

            var edit PageEdit
            err = json.Unmarshal([]byte(unescaped), &edit)
            if err != nil {
                return []PageEdit{}, err
            }

            result = append(result, edit)
        }
    }

    return result, nil
}

func PageEditToBytes(edit PageEdit) ([]byte, error) {

    editJsonBytes, err := json.Marshal(edit)
    if err != nil {
        return nil, err
    }

    editJson := string(editJsonBytes)

    escaped := url.QueryEscape(editJson)

    return []byte(escaped + "\n"), nil
}

func WriteStringToPageShard(
    pageName string,
    editor string,
    str string,
    hash string,
    accessToken string,
) error {
    pageName = strings.ToLower(pageName)
    ts := strconv.FormatUint(uint64(time.Now().UnixNano()), 10)
    centralToken := utils.Config()["DROPBOX_CENTRAL_ACCOUNT_TOKEN"]
    edit := PageEdit{
        PageName: pageName,
        Editor: editor,
        Timestamp: ts,
        Diff: str,
        PrevHash: hash,
    }
    editBytes, err := PageEditToBytes(edit)
    if err != nil {
        return err
    }

    fileName := "/dbpedia_data:" + editor + "/" + pageName
    var shardExists = true

    resp, err := utils.MakeRequest(
        "https:
        "POST",
        []byte{},
        map[string]string{
            "Authorization": "Bearer " + accessToken,
            "Dropbox-API-Arg": `{ "path": "` + fileName + `" }`,
        },
    )
    if err != nil {
        return err
    } else {
        defer resp.Body.Close()
    }

    currFileBody, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return err
    }

    if strings.HasPrefix(resp.Status, "200") {
        shardExists = true
    } else if strings.HasPrefix(resp.Status, "409") {

        shardExists = false
    } else {
        return errors.New("Something happened trying to get dropbox file")
    }

    var currentShard string
    if shardExists {

        currentShard = string(currFileBody)
    } else {
        currentShard = ""
    }

    newShard := currentShard + string(editBytes)

    reqData := `{ "path": "` + fileName + `", "mode": "overwrite", "mute": true }`
    resp, err = utils.MakeRequest(
        "https:
        "POST",
        []byte(newShard),
        map[string]string{
            "Authorization": "Bearer " + accessToken,
            "Dropbox-API-Arg": reqData,
            "Content-Type": "application/octet-stream",
        },
    )
    if err != nil {
        return err
    } else {
        defer resp.Body.Close()
    }

    s, _ := ioutil.ReadAll(resp.Body)
    utils.Debug(string(s))

    indexData, err := GetIndex()
    if err != nil {
        utils.Error("Error getting index")
        return err
    }
    exists := false

    pageEditors := indexData.Pages[pageName]
    for _, pageEditor := range pageEditors {
        if pageEditor == editor {
            exists = true
        }
    }

    if exists {
        return nil
    }

    indexData.Pages[pageName] = append(indexData.Pages[pageName], editor)
    reqData = `{ "path": "/dbpedia_index.json", "mode": "overwrite", "mute": true }`
    jsonBytes, err := json.Marshal(indexData)

    if err != nil {
        utils.Error("Error turning indexData to JSON")
        return err
    }

    resp, err = utils.MakeRequest(
        "https:
        "POST",
        jsonBytes,
        map[string]string{
            "Authorization": "Bearer " + centralToken,
            "Dropbox-API-Arg": reqData,
            "Content-Type": "application/octet-stream",
        },
    )

    if err != nil {
        return err
    } else {
        defer resp.Body.Close()
    }

    return nil
}

func PageDataHash(text string) string {
    hash := md5.New()
    hash.Write([]byte(text))
    return hex.EncodeToString(hash.Sum(nil))
}

func GetPageFromShards(shards []PageShard) (string, error) {

    var sortedEdits []PageEdit
    for _, shard := range shards {
        for _, edit := range shard.Edits {
            sortedEdits = append(sortedEdits, edit)
        }
    }

    sort.Slice(
        sortedEdits,
        func(x, y int) bool {
            tx, errX := strconv.ParseInt(sortedEdits[x].Timestamp, 10, 64)
            ty, errY := strconv.ParseInt(sortedEdits[y].Timestamp, 10, 64)
            return errX == nil && errY == nil && tx < ty
        },
    )

    var pageData string
    var correctEdits []PageEdit
    dmp := diffmatchpatch.New()

    for _, edit := range sortedEdits {

        checkHash := PageDataHash(pageData)

        if edit.PrevHash == checkHash {
            correctEdits = append(correctEdits, edit)

            diffs, err := dmp.DiffFromDelta(pageData, edit.Diff)

            if err != nil {
                utils.Error("Failed to create diff objects from passed string")
                return "", errors.New("Failed to create diff")
            }
            patches := dmp.PatchMake(pageData, diffs)

            pageData, _ = dmp.PatchApply(patches, pageData)
            utils.Debug(pageData)
        }
    }

    return pageData, nil
}

func GetPageData(pageName string) (string, error) {
    pageName = strings.ToLower(pageName)

    shards, err := GetPageShards(pageName)
    if err != nil {
        return "", errors.New("Unable to retrieve page shards.")
    }

    pageData, err := GetPageFromShards(shards)
    if err != nil {
        return "", errors.New("Unable to turn shards into page data.")
    }

    return pageData, nil
}

// Functions for creating and editing pages.

package dropbox

import (
    "encoding/json"
    "encoding/hex"
    "strings"
    "io/ioutil"
    "time"
    "strconv"
    "net/url"
    "errors"
    "sort"
    "crypto/md5"

    "github.com/aniiyengar/dbpedia/api/utils"
    "github.com/sergi/go-diff/diffmatchpatch"
)

// A "page shard" is one editor's contribution to one page.
// It will have a certain number of edits.
type PageShard struct {
    PageName string
    Editor string
    Edits []PageEdit
}

// A page edit has a timestamp and associated diff
type PageEdit struct {
    PageName string     `json:"page_name"`
    Editor string       `json:"editor"`
    Timestamp string    `json:"timestamp"`
    PrevHash string     `json:"prev_hash"`

    // Diff is a crushed string generated by diff_toDelta.
    Diff string         `json:"diff"`
}

type Page struct {
    Title string `json:"page_name"`
    Data string `json:"page_data"`
}

// All this does is add page to index.
func AddPageToIndexIfNotExists(pageName string) error {
    pageName = strings.ToLower(pageName)

    indexData, err := GetIndex()
    if err != nil {
        return err
    }

    exists := false
    pageList := indexData.Pages
    for name, _ := range pageList {
        if name == pageName {
            exists = true
            break
        }
    }

    if exists {
        // Nothing to do here.
        return nil
    } else {
        indexData.Pages[pageName] = []string{}
    }

    centralToken := utils.Config()["DROPBOX_CENTRAL_ACCOUNT_TOKEN"]

    jsonBytes, err := json.Marshal(indexData)
    reqJson := []byte(`{ "path": "/dbpedia_index.json", "mode": "overwrite", "mute": true }`)
    resp, err := utils.MakeRequest(
        "https://content.dropboxapi.com/2/files/upload",
        "POST",
        jsonBytes,
        map[string]string{
            "Content-Type": "application/octet-stream",
            "Dropbox-API-Arg": string(reqJson),
            "Authorization": "Bearer " + centralToken,
        },
    )
    if err != nil {
        return err
    } else {
        defer resp.Body.Close()
    }

    return nil
}

// If a page exists, return the Page. Otherwise return Page{}.
func GetPageShards(pageName string) ([]PageShard, error) {
    // First, get the page from the index. If it doesn't exist,
    // return a blank Page.
    pageName = strings.ToLower(pageName)

    centralToken := utils.Config()["DROPBOX_CENTRAL_ACCOUNT_TOKEN"]
    indexData, err := GetIndex()
    if err != nil {
        return []PageShard{}, err
    }

    exists := false
    pageList := indexData.Pages
    for name, _ := range pageList {
        if strings.ToLower(name) == pageName {
            exists = true
            break
        }
    }

    if !exists {
        return []PageShard{}, nil
    }

    // If the page does exist, get the list of editors
    editors := indexData.Pages[pageName]
    var requestsToMake []utils.Request

    utils.Debug(pageName)

    for _, editor := range editors {
        // The editor's contribution to the page will be in
        // /dbpedia_data:[dbid]/[pageName].
        fileName := "/dbpedia_data:" + editor + "/" + pageName
        newReq := utils.Request{
            Url: "https://content.dropboxapi.com/2/files/download",
            Method: "POST",
            Data: []byte{},
            Headers: map[string]string{
                "Authorization": "Bearer " + centralToken,
                "Dropbox-API-Arg": "{ \"path\": \"" + fileName + "\" }",
            },
        }
        requestsToMake = append(requestsToMake, newReq)
    }

    var reqResults []utils.ParallelRequestResponse
    reqResults = utils.MakeParallelRequests(requestsToMake, 8) // is this ok?

    // Turn each reqResult into a page shard
    var returnValue []PageShard
    for _, res := range reqResults {
        if res.Error != nil {
            return []PageShard{}, res.Error
        } else {
            defer res.Response.Body.Close()
        }

        resData, err := ioutil.ReadAll(res.Response.Body)
        if err != nil {
            return []PageShard{}, err
        }

        edits, err := BytesToPageEdits(resData)
        if err != nil {
            return []PageShard{}, err
        }

        pageShard := PageShard{
            PageName: pageName,
            Editor: editors[res.RequestIndex],
            Edits: edits,
        }
        returnValue = append(returnValue, pageShard)
    }

    return returnValue, nil
}

type pageEditSerialization struct {
    edits []PageEdit
}

// Turns page edits into data.
func PageEditsToBytes(edits []PageEdit) ([]byte, error) {
    var resultString string

    for _, edit := range edits {
        bts, err := PageEditToBytes(edit)
        if err != nil {
            return nil, err
        }

        resultString = resultString + string(bts)
    }

    return []byte(resultString), nil
}

// Turns data into list of page edits.
func BytesToPageEdits(data []byte) ([]PageEdit, error) {
    dataString := string(data)
    editStrings := strings.Split(dataString, "\n")
    var result []PageEdit

    for _, editString := range editStrings {
        if len(editString) != 0 {
            // Each edit needs to first be url unescaped
            unescaped, err := url.QueryUnescape(editString)
            if err != nil {
                return []PageEdit{}, err
            }

            // Turn the unescaped string back to object
            var edit PageEdit
            err = json.Unmarshal([]byte(unescaped), &edit)
            if err != nil {
                return []PageEdit{}, err
            }

            result = append(result, edit)
        }
    }

    return result, nil
}

// Turns one page edit into a byte array.
func PageEditToBytes(edit PageEdit) ([]byte, error) {
    // Turn the edit into JSON
    editJsonBytes, err := json.Marshal(edit)
    if err != nil {
        return nil, err
    }

    editJson := string(editJsonBytes)

    // Urlencode the JSON onto one line
    escaped := url.QueryEscape(editJson)

    return []byte(escaped + "\n"), nil
}

// Write to edit file.
func WriteStringToPageShard(
    pageName string,
    editor string,
    str string,
    hash string,
    accessToken string,
) error {
    pageName = strings.ToLower(pageName)
    ts := strconv.FormatUint(uint64(time.Now().UnixNano()), 10)
    centralToken := utils.Config()["DROPBOX_CENTRAL_ACCOUNT_TOKEN"]
    edit := PageEdit{
        PageName: pageName,
        Editor: editor,
        Timestamp: ts,
        Diff: str,
        PrevHash: hash,
    }
    editBytes, err := PageEditToBytes(edit)
    if err != nil {
        return err
    }

    // Get the current file, append string, and reupload
    fileName := "/dbpedia_data:" + editor + "/" + pageName
    var shardExists = true

    // Get current file:
    resp, err := utils.MakeRequest(
        "https://content.dropboxapi.com/2/files/download",
        "POST",
        []byte{},
        map[string]string{
            "Authorization": "Bearer " + accessToken,
            "Dropbox-API-Arg": `{ "path": "` + fileName + `" }`,
        },
    )
    if err != nil {
        return err
    } else {
        defer resp.Body.Close()
    }

    currFileBody, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return err
    }

    // Get body; see if file exists
    if strings.HasPrefix(resp.Status, "200") {
        shardExists = true
    } else if strings.HasPrefix(resp.Status, "409") {
        // File not found; it's okay though
        shardExists = false
    } else {
        return errors.New("Something happened trying to get dropbox file")
    }

    var currentShard string
    if shardExists {
        // currFileBody has the data
        currentShard = string(currFileBody)
    } else {
        currentShard = ""
    }

    newShard := currentShard + string(editBytes)

    // Finally write to page shard
    reqData := `{ "path": "` + fileName + `", "mode": "overwrite", "mute": true }`
    resp, err = utils.MakeRequest(
        "https://content.dropboxapi.com/2/files/upload",
        "POST",
        []byte(newShard),
        map[string]string{
            "Authorization": "Bearer " + accessToken,
            "Dropbox-API-Arg": reqData,
            "Content-Type": "application/octet-stream",
        },
    )
    if err != nil {
        return err
    } else {
        defer resp.Body.Close()
    }

    s, _ := ioutil.ReadAll(resp.Body)
    utils.Debug(string(s))

    // Lastly, we have to add the editor to the index on that page.
    indexData, err := GetIndex()
    if err != nil {
        utils.Error("Error getting index")
        return err
    }
    exists := false

    pageEditors := indexData.Pages[pageName]
    for _, pageEditor := range pageEditors {
        if pageEditor == editor {
            exists = true
        }
    }

    if exists {
        return nil
    }

    // If the user isn't in the index for that page,
    // we have to add them
    indexData.Pages[pageName] = append(indexData.Pages[pageName], editor)
    reqData = `{ "path": "/dbpedia_index.json", "mode": "overwrite", "mute": true }`
    jsonBytes, err := json.Marshal(indexData)

    if err != nil {
        utils.Error("Error turning indexData to JSON")
        return err
    }

    resp, err = utils.MakeRequest(
        "https://content.dropboxapi.com/2/files/upload",
        "POST",
        jsonBytes,
        map[string]string{
            "Authorization": "Bearer " + centralToken,
            "Dropbox-API-Arg": reqData,
            "Content-Type": "application/octet-stream",
        },
    )

    if err != nil {
        return err
    } else {
        defer resp.Body.Close()
    }

    return nil
}

func PageDataHash(text string) string {
    hash := md5.New()
    hash.Write([]byte(text))
    return hex.EncodeToString(hash.Sum(nil))
}

func GetPageFromShards(shards []PageShard) (string, error) {
    // Given the shard collection,
    // 1. Order the edits in all the shards by timestamp.
    // 2. Iterate through the edits. After each one, make sure
    //    the PrevHash is consistent with the page text that's
    //    generated. If it isn't, skip the edit.
    // 3. Return the page text.

    var sortedEdits []PageEdit
    for _, shard := range shards {
        for _, edit := range shard.Edits {
            sortedEdits = append(sortedEdits, edit)
        }
    }

    sort.Slice(
        sortedEdits,
        func(x, y int) bool {
            tx, errX := strconv.ParseInt(sortedEdits[x].Timestamp, 10, 64)
            ty, errY := strconv.ParseInt(sortedEdits[y].Timestamp, 10, 64)
            return errX == nil && errY == nil && tx < ty
        },
    )

    // PageData starts empty. As we go through the diffs, we add more
    // to it and continually check the hash.
    var pageData string
    var correctEdits []PageEdit
    dmp := diffmatchpatch.New()

    for _, edit := range sortedEdits {
        // Calculate hash of current page
        checkHash := PageDataHash(pageData)

        // Add edit if hash is valid
        if edit.PrevHash == checkHash {
            correctEdits = append(correctEdits, edit)

            // Turn edit diff into patch
            diffs, err := dmp.DiffFromDelta(pageData, edit.Diff)

            if err != nil {
                utils.Error("Failed to create diff objects from passed string")
                return "", errors.New("Failed to create diff")
            }
            patches := dmp.PatchMake(pageData, diffs)

            pageData, _ = dmp.PatchApply(patches, pageData)
            utils.Debug(pageData)
        }
    }

    return pageData, nil
}

func GetPageData(pageName string) (string, error) {
    pageName = strings.ToLower(pageName)
    
    shards, err := GetPageShards(pageName)
    if err != nil {
        return "", errors.New("Unable to retrieve page shards.")
    }

    pageData, err := GetPageFromShards(shards)
    if err != nil {
        return "", errors.New("Unable to turn shards into page data.")
    }

    return pageData, nil
}
