package admin

import (
	"context"
	"database/sql"
	"embed"
	"fmt"
	"mime"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"strings"
	"time"

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

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

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

    mux.Handle("/qr-test", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        qrB64Img, err := controller.GenerateQRCode("super epic mega gaming test message. be sure to buy free2play on bandcamp so i can put food on my family")
        if err != nil {
            fmt.Fprintf(os.Stderr, "WARN: Failed to generate QR code: %v\n", err)
            http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
            return
        }

        w.Write([]byte("<html><img style=\"image-rendering:pixelated;width:100%;height:100%;object-fit:contain\" src=\"" + qrB64Img + "\"/></html>"))
    }))

    mux.Handle("/login", loginHandler(app))
    mux.Handle("/totp", loginTOTPHandler(app))
    mux.Handle("/logout", requireAccount(logoutHandler(app)))

    mux.Handle("/register", registerAccountHandler(app))

    mux.Handle("/account", requireAccount(accountIndexHandler(app)))
    mux.Handle("/account/", requireAccount(http.StripPrefix("/account", accountHandler(app))))

    mux.Handle("/logs", requireAccount(logsHandler(app)))

    mux.Handle("/release/", requireAccount(http.StripPrefix("/release", serveRelease(app))))
    mux.Handle("/artist/", requireAccount(http.StripPrefix("/artist", serveArtist(app))))
    mux.Handle("/track/", requireAccount(http.StripPrefix("/track", serveTrack(app))))

    mux.Handle("/static/", http.StripPrefix("/static", staticHandler()))

    mux.Handle("/", requireAccount(AdminIndexHandler(app)))

    // response wrapper to make sure a session cookie exists
    return enforceSession(app, mux)
}

func AdminIndexHandler(app *model.AppState) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path != "/" {
            http.NotFound(w, r)
            return
        }

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

        releases, err := controller.GetAllReleases(app.DB, false, 0, true)
        if err != nil {
            fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases: %s\n", err)
            http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
            return
        }

        artists, err := controller.GetAllArtists(app.DB)
        if err != nil {
            fmt.Fprintf(os.Stderr, "WARN: Failed to pull artists: %s\n", err)
            http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
            return
        }

        tracks, err := controller.GetOrphanTracks(app.DB)
        if err != nil {
            fmt.Fprintf(os.Stderr, "WARN: Failed to pull orphan tracks: %s\n", err)
            http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
            return
        }

        type IndexData struct {
            Session     *model.Session
            Releases    []*model.Release
            Artists     []*model.Artist
            Tracks      []*model.Track
        }

        err = templates.IndexTemplate.Execute(w, IndexData{
            Session: session,
            Releases: releases,
            Artists: artists,
            Tracks: tracks,
        })
        if err != nil {
            fmt.Fprintf(os.Stderr, "WARN: Failed to render admin index: %s\n", err)
            http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
            return
        }
    })
}

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

        if session.Account != nil {
            // user is already logged in
            http.Redirect(w, r, "/admin", http.StatusFound)
            return
        }

        type registerData struct {
            Session *model.Session
        }

        render := func() {
            err := templates.RegisterTemplate.Execute(w, registerData{ Session: session })
            if err != nil {
                fmt.Printf("WARN: Error rendering create account page: %s\n", err)
                http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
            }
        }

        if r.Method == http.MethodGet {
            render()
            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
        }

        type RegisterRequest struct {
            Username    string `json:"username"`
            Email       string `json:"email"`
            Password    string `json:"password"`
            Invite      string `json:"invite"`
        }
        credentials := RegisterRequest{
            Username: r.Form.Get("username"),
            Email: r.Form.Get("email"),
            Password: r.Form.Get("password"),
            Invite: r.Form.Get("invite"),
        }

        // make sure invite code exists in DB
        invite, err := controller.GetInvite(app.DB, credentials.Invite)
        if err != nil {
            fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve invite: %v\n", err)
            controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
            render()
            return
        }
        if invite == nil || time.Now().After(invite.ExpiresAt) {
            if invite != nil {
                err := controller.DeleteInvite(app.DB, invite.Code)
                if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %v\n", err) }
            }
            controller.SetSessionError(app.DB, session, "Invalid invite code.")
            render()
            return
        }

        hashedPassword, err := bcrypt.GenerateFromPassword([]byte(credentials.Password), 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.")
            render()
            return
        }

        account := model.Account{
            Username: credentials.Username,
            Password: string(hashedPassword),
            Email: sql.NullString{ String: credentials.Email, Valid: true },
            AvatarURL: sql.NullString{ String:  "/img/default-avatar.png", Valid: true },
        }
        err = controller.CreateAccount(app.DB, &account)
        if err != nil {
            if strings.HasPrefix(err.Error(), "pq: duplicate key") {
                controller.SetSessionError(app.DB, session, "An account with that username already exists.")
                render()
                return
            }
            fmt.Fprintf(os.Stderr, "WARN: Failed to create account: %v\n", err)
            controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
            render()
            return
        }

        app.Log.Info(log.TYPE_ACCOUNT, "Account \"%s\" (%s) created using invite \"%s\". (%s)", account.Username, account.ID, invite.Code, controller.ResolveIP(app, r))

        err = controller.DeleteInvite(app.DB, invite.Code)
        if err != nil {
            app.Log.Warn(log.TYPE_ACCOUNT, "Failed to delete expired invite \"%s\": %v", invite.Code, err)
        }

        // registration success!
        controller.SetSessionAccount(app.DB, session, &account)
        controller.SetSessionMessage(app.DB, session, "")
        controller.SetSessionError(app.DB, session, "")
        http.Redirect(w, r, "/admin", http.StatusFound)
    })
}

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

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

        type loginData struct {
            Session *model.Session
        }

        render := func() {
            err := templates.LoginTemplate.Execute(w, loginData{ Session: session })
            if err != nil {
                fmt.Fprintf(os.Stderr, "WARN: Error rendering admin login page: %s\n", err)
                http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
                return
            }
        }

        if r.Method == http.MethodGet {
            if session.Account != nil {
                // user is already logged in
                http.Redirect(w, r, "/admin", http.StatusFound)
                return
            }
            render()
            return
        }

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

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

        username := r.FormValue("username")
        password := r.FormValue("password")

        account, err := controller.GetAccountByUsername(app.DB, username)
        if err != nil {
            fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account for login: %v\n", err)
            controller.SetSessionError(app.DB, session, "Invalid username or password.")
            render()
            return
        }
        if account == nil {
            controller.SetSessionError(app.DB, session, "Invalid username or password.")
            render()
            return
        }
        if account.Locked {
            controller.SetSessionError(app.DB, session, "This account is locked.")
            render()
            return
        }

        err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(password))
        if err != nil {
            app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" attempted login with incorrect password. (%s)", account.Username, controller.ResolveIP(app, r))
            if locked := handleFailedLogin(app, account, r); locked {
                controller.SetSessionError(app.DB, session, "Too many failed attempts. This account is now locked.")
            } else {
                controller.SetSessionError(app.DB, session, "Invalid username or password.")
            }
            render()
            return
        }

        totps, err := controller.GetTOTPsForAccount(app.DB, account.ID)
        if err != nil {
            fmt.Fprintf(os.Stderr, "WARN: Failed to fetch TOTPs: %v\n", err)
            controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
            render()
            return
        }

        if len(totps) > 0 {
            err = controller.SetSessionAttemptAccount(app.DB, session, account)
            if err != nil {
                fmt.Fprintf(os.Stderr, "WARN: Failed to set attempt session: %v\n", err)
                controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
                render()
                return
            }
            controller.SetSessionMessage(app.DB, session, "")
            controller.SetSessionError(app.DB, session, "")
            http.Redirect(w, r, "/admin/totp", http.StatusFound)
            return
        }

        // login success!
        // TODO: log login activity to user
        app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" logged in. (%s)", account.Username, controller.ResolveIP(app, r))
        app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" does not have any TOTP methods assigned.", account.Username)

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

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

        if session.AttemptAccount == nil {
            http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
            return
        }

        type loginTOTPData struct {
            Session *model.Session
        }

        render := func() {
            err := templates.LoginTOTPTemplate.Execute(w, loginTOTPData{ Session: session })
            if err != nil {
                fmt.Fprintf(os.Stderr, "WARN: Failed to render login TOTP page: %v\n", err)
                http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
                return
            }
        }

        if r.Method == http.MethodGet {
            render()
            return
        }

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

        r.ParseForm()

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

        totpCode := r.FormValue("totp")

        if len(totpCode) != controller.TOTP_CODE_LENGTH {
            app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" failed login (Invalid TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(app, r))
            controller.SetSessionError(app.DB, session, "Invalid TOTP.")
            render()
            return
        }

        totpMethod, err := controller.CheckTOTPForAccount(app.DB, session.AttemptAccount.ID, totpCode)
        if err != nil {
            fmt.Fprintf(os.Stderr, "WARN: Failed to check TOTPs: %v\n", err)
            controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
            render()
            return
        }
        if totpMethod == nil {
            app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" failed login (Incorrect TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(app, r))
            if locked := handleFailedLogin(app, session.AttemptAccount, r); locked {
                controller.SetSessionError(app.DB, session, "Too many failed attempts. This account is now locked.")
                controller.SetSessionAttemptAccount(app.DB, session, nil)
                http.Redirect(w, r, "/admin", http.StatusFound)
            } else {
                controller.SetSessionError(app.DB, session, "Incorrect TOTP.")
            }
            render()
            return
        }

        app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" logged in with TOTP method \"%s\". (%s)", session.AttemptAccount.Username, totpMethod.Name, controller.ResolveIP(app, r))

        err = controller.SetSessionAccount(app.DB, session, session.AttemptAccount)
        if err != nil {
            fmt.Fprintf(os.Stderr, "WARN: Failed to set session account: %v\n", err)
            controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
            render()
            return
        }
        err = controller.SetSessionAttemptAccount(app.DB, session, nil)
        if err != nil {
            fmt.Fprintf(os.Stderr, "WARN: Failed to clear attempt session: %v\n", err)
        }
        controller.SetSessionMessage(app.DB, session, "")
        controller.SetSessionError(app.DB, session, "")
        http.Redirect(w, r, "/admin", http.StatusFound)
    })
}

func logoutHandler(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
        }

        session := r.Context().Value("session").(*model.Session)
        err := controller.DeleteSession(app.DB, session.Token)
        if err != nil {
            fmt.Fprintf(os.Stderr, "WARN: Failed to delete session: %v\n", err)
            http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
            return
        }

        http.SetCookie(w, &http.Cookie{
            Name: model.COOKIE_TOKEN,
            Expires: time.Now(),
            Path: "/",
        })

        err = templates.LogoutTemplate.Execute(w, nil)
        if err != nil {
            fmt.Fprintf(os.Stderr, "WARN: Failed to render logout page: %v\n", err)
            http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
        }
    })
}

func requireAccount(next http.Handler) http.HandlerFunc {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        session := r.Context().Value("session").(*model.Session)
        if session.Account == nil {
            // TODO: include context in redirect
            http.Redirect(w, r, "/admin/login", http.StatusFound)
            return
        }
        next.ServeHTTP(w, r)
    })
}

//go:embed "static"
var staticFS embed.FS

func staticHandler() http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        file, err := staticFS.ReadFile(filepath.Join("static", filepath.Clean(r.URL.Path)))
        if err != nil {
            http.NotFound(w, r)
            return
        }

        w.Header().Set("Content-Type", mime.TypeByExtension(path.Ext(r.URL.Path)))
        w.WriteHeader(http.StatusOK)

        w.Write(file)
    })
}

func enforceSession(app *model.AppState, next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        session, err := controller.GetSessionFromRequest(app, r)
        if err != nil {
            fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve session: %v\n", err)
            http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
            return
        }

        if session == nil {
            // create a new session
            session, err = controller.CreateSession(app.DB, r.UserAgent())
            if err != nil {
                fmt.Fprintf(os.Stderr, "WARN: Failed to create session: %v\n", err)
                http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
                return
            }

            http.SetCookie(w, &http.Cookie{
                Name: model.COOKIE_TOKEN,
                Value: session.Token,
                Expires: session.ExpiresAt,
                Secure: strings.HasPrefix(app.Config.BaseUrl, "https"),
                HttpOnly: true,
                Path: "/",
            })
        }

        ctx := context.WithValue(r.Context(), "session", session)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func handleFailedLogin(app *model.AppState, account *model.Account, r *http.Request) bool {
    locked, err := controller.IncrementAccountFails(app.DB, account.ID)
    if err != nil {
        fmt.Fprintf(
            os.Stderr,
            "WARN: Failed to increment login failures for \"%s\": %v\n",
            account.Username,
            err,
        )
        app.Log.Warn(
            log.TYPE_ACCOUNT,
            "Failed to increment login failures for \"%s\"",
            account.Username,
        )
    }
    if locked {
        app.Log.Warn(
            log.TYPE_ACCOUNT,
            "Account \"%s\" was locked: %d failed login attempts (IP: %s)",
            account.Username,
            model.MAX_LOGIN_FAIL_ATTEMPTS,
            controller.ResolveIP(app, r),
        )
    }
    return locked
}
