package admin

import (
    "database/sql"
    "fmt"
    "net/http"
    "net/url"
    "os"

    "arimelody-web/controller"
    "arimelody-web/log"
    "arimelody-web/model"

    "golang.org/x/crypto/bcrypt"
)

func accountHandler(app *model.AppState) http.Handler {
    mux := http.NewServeMux()

    mux.Handle("/totp-setup", totpSetupHandler(app))
    mux.Handle("/totp-confirm", totpConfirmHandler(app))
    mux.Handle("/totp-delete/", http.StripPrefix("/totp-delete", totpDeleteHandler(app)))

    mux.Handle("/password", changePasswordHandler(app))
    mux.Handle("/delete", deleteAccountHandler(app))

    return mux
}

func accountIndexHandler(app *model.AppState) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        session := r.Context().Value("session").(*model.Session)

        dbTOTPs, err := controller.GetTOTPsForAccount(app.DB, session.Account.ID)
        if err != nil {
            fmt.Printf("WARN: Failed to fetch TOTPs: %v\n", err)
            http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
        }

        type (
            TOTP struct {
                model.TOTP
                CreatedAtString string
            }

            accountResponse struct {
                Session *model.Session
                TOTPs []TOTP
            }
        )

        totps := []TOTP{}
        for _, totp := range dbTOTPs { 
            totps = append(totps, TOTP{
                TOTP: totp,
                CreatedAtString: totp.CreatedAt.Format("02 Jan 2006, 15:04:05"),
            })
        }

        sessionMessage := session.Message
        sessionError := session.Error
        controller.SetSessionMessage(app.DB, session, "")
        controller.SetSessionError(app.DB, session, "")
        session.Message = sessionMessage
        session.Error = sessionError

        err = accountTemplate.Execute(w, accountResponse{
            Session: session,
            TOTPs: totps,
        })
        if err != nil {
            fmt.Printf("WARN: Failed to render admin account page: %v\n", err)
            http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
        }
    })
}

func changePasswordHandler(app *model.AppState) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodPost {
            http.NotFound(w, r)
            return
        }

        session := r.Context().Value("session").(*model.Session)

        controller.SetSessionMessage(app.DB, session, "")
        controller.SetSessionError(app.DB, session, "")

        r.ParseForm()

        currentPassword := r.Form.Get("current-password")
        if err := bcrypt.CompareHashAndPassword([]byte(session.Account.Password), []byte(currentPassword)); err != nil {
            controller.SetSessionError(app.DB, session, "Incorrect password.")
            http.Redirect(w, r, "/admin/account", http.StatusFound)
            return
        }

        newPassword := r.Form.Get("new-password")

        hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
        if err != nil {
            fmt.Fprintf(os.Stderr, "WARN: Failed to generate password hash: %v\n", err)
            controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
            http.Redirect(w, r, "/admin/account", http.StatusFound)
            return
        }

        session.Account.Password = string(hashedPassword)
        err = controller.UpdateAccount(app.DB, session.Account)
        if err != nil {
            fmt.Fprintf(os.Stderr, "WARN: Failed to update account password: %v\n", err)
            controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
            http.Redirect(w, r, "/admin/account", http.StatusFound)
            return
        }

        app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" changed password by user request. (%s)", session.Account.Username, controller.ResolveIP(app, r))

        controller.SetSessionError(app.DB, session, "")
        controller.SetSessionMessage(app.DB, session, "Password updated successfully.")
        http.Redirect(w, r, "/admin/account", http.StatusFound)
    })
}

func deleteAccountHandler(app *model.AppState) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodPost {
            http.NotFound(w, r)
            return
        }

        err := r.ParseForm()
        if err != nil {
            http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
            return
        }

        if !r.Form.Has("password") {
            http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
            return
        }

        session := r.Context().Value("session").(*model.Session)

        // check password
        if err := bcrypt.CompareHashAndPassword([]byte(session.Account.Password), []byte(r.Form.Get("password"))); err != nil {
            app.Log.Warn(log.TYPE_ACCOUNT, "Account \"%s\" attempted account deletion with incorrect password. (%s)", session.Account.Username, controller.ResolveIP(app, r))
            controller.SetSessionError(app.DB, session, "Incorrect password.")
            http.Redirect(w, r, "/admin/account", http.StatusFound)
            return
        }

        err = controller.DeleteAccount(app.DB, session.Account.ID)
        if err != nil {
            fmt.Fprintf(os.Stderr, "Failed to delete account: %v\n", err)
            controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
            http.Redirect(w, r, "/admin/account", http.StatusFound)
            return
        }

        app.Log.Info(log.TYPE_ACCOUNT, "Account \"%s\" deleted by user request. (%s)", session.Account.Username, controller.ResolveIP(app, r))

        controller.SetSessionAccount(app.DB, session, nil)
        controller.SetSessionError(app.DB, session, "")
        controller.SetSessionMessage(app.DB, session, "Account deleted successfully.")
        http.Redirect(w, r, "/admin/login", http.StatusFound)
    })
}

type totpConfirmData struct {
    Session *model.Session
    TOTP *model.TOTP
    NameEscaped string
    QRBase64Image string
}

func totpSetupHandler(app *model.AppState) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Method == http.MethodGet {
            type totpSetupData struct {
                Session *model.Session
            }

            session := r.Context().Value("session").(*model.Session)

            err := totpSetupTemplate.Execute(w, totpSetupData{ Session: session })
            if err != nil {
                fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err)
                http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
            }
            return
        }

        if r.Method != http.MethodPost {
            http.NotFound(w, r)
            return
        }

        err := r.ParseForm()
        if err != nil {
            http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
            return
        }

        name := r.FormValue("totp-name")
        if len(name) == 0 {
            http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
            return
        }

        session := r.Context().Value("session").(*model.Session)

        secret := controller.GenerateTOTPSecret(controller.TOTP_SECRET_LENGTH)
        totp := model.TOTP {
            AccountID: session.Account.ID,
            Name: name,
            Secret: string(secret),
        }
        err = controller.CreateTOTP(app.DB, &totp)
        if err != nil {
            fmt.Printf("WARN: Failed to create TOTP method: %s\n", err)
            controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
            err := totpSetupTemplate.Execute(w, totpConfirmData{ Session: session })
            if err != nil {
                fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err)
                http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
            }
            return
        }

        qrBase64Image, err := controller.GenerateQRCode(
            controller.GenerateTOTPURI(session.Account.Username, totp.Secret))
        if err != nil {
            fmt.Fprintf(os.Stderr, "WARN: Failed to generate TOTP QR code: %v\n", err)
        }

        err = totpConfirmTemplate.Execute(w, totpConfirmData{
            Session: session,
            TOTP: &totp,
            NameEscaped: url.PathEscape(totp.Name),
            QRBase64Image: qrBase64Image,
        })
        if err != nil {
            fmt.Printf("WARN: Failed to render TOTP confirm page: %s\n", err)
            http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
        }
    })
}

func totpConfirmHandler(app *model.AppState) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodPost {
            http.NotFound(w, r)
            return
        }

        session := r.Context().Value("session").(*model.Session)

        err := r.ParseForm()
        if err != nil {
            http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
            return
        }
        name := r.FormValue("totp-name")
        if len(name) == 0 {
            http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
            return
        }
        code := r.FormValue("totp")
        if len(code) != controller.TOTP_CODE_LENGTH {
            http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
            return
        }

        totp, err := controller.GetTOTP(app.DB, session.Account.ID, name)
        if err != nil {
            fmt.Printf("WARN: Failed to fetch TOTP method: %v\n", err)
            controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
            http.Redirect(w, r, "/admin/account", http.StatusFound)
            return
        }
        if totp == nil {
            http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
            return
        }

        qrBase64Image, err := controller.GenerateQRCode(
            controller.GenerateTOTPURI(session.Account.Username, totp.Secret))
        if err != nil {
            fmt.Fprintf(os.Stderr, "WARN: Failed to generate TOTP QR code: %v\n", err)
        }

        confirmCode := controller.GenerateTOTP(totp.Secret, 0)
        if code != confirmCode {
            confirmCodeOffset := controller.GenerateTOTP(totp.Secret, 1)
            if code != confirmCodeOffset {
                session.Error = sql.NullString{ Valid: true, String: "Incorrect TOTP code. Please try again." }
                err = totpConfirmTemplate.Execute(w, totpConfirmData{
                    Session: session,
                    TOTP: totp,
                    NameEscaped: url.PathEscape(totp.Name),
                    QRBase64Image: qrBase64Image,
                })
                if err != nil {
                    fmt.Fprintf(os.Stderr, "WARN: Failed to render TOTP setup page: %v\n", err)
                    http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
                }
                return
            }
        }

        err = controller.ConfirmTOTP(app.DB, session.Account.ID, name)
        if err != nil {
            fmt.Printf("WARN: Failed to confirm TOTP method: %s\n", err)
            controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
            http.Redirect(w, r, "/admin/account", http.StatusFound)
            return
        }

        app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" created TOTP method \"%s\".", session.Account.Username, totp.Name)

        controller.SetSessionError(app.DB, session, "")
        controller.SetSessionMessage(app.DB, session, fmt.Sprintf("TOTP method \"%s\" created successfully.", totp.Name))
        http.Redirect(w, r, "/admin/account", http.StatusFound)
    })
}

func totpDeleteHandler(app *model.AppState) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodGet {
            http.NotFound(w, r)
            return
        }

        if len(r.URL.Path) < 2 {
            http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
            return
        }
        name := r.URL.Path[1:]

        session := r.Context().Value("session").(*model.Session)

        totp, err := controller.GetTOTP(app.DB, session.Account.ID, name)
        if err != nil {
            fmt.Printf("WARN: Failed to fetch TOTP method: %s\n", err)
            controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
            http.Redirect(w, r, "/admin/account", http.StatusFound)
            return
        }
        if totp == nil {
            http.NotFound(w, r)
            return
        }

        err = controller.DeleteTOTP(app.DB, session.Account.ID, totp.Name)
        if err != nil {
            fmt.Printf("WARN: Failed to delete TOTP method: %s\n", err)
            controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
            http.Redirect(w, r, "/admin/account", http.StatusFound)
            return
        }

        app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" deleted TOTP method \"%s\".", session.Account.Username, totp.Name)

        controller.SetSessionError(app.DB, session, "")
        controller.SetSessionMessage(app.DB, session, fmt.Sprintf("TOTP method \"%s\" deleted successfully.", totp.Name))
        http.Redirect(w, r, "/admin/account", http.StatusFound)
    })
}
