package admin

import (
	"context"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"arimelody-web/discord"
	"arimelody-web/global"
	"arimelody-web/controller"
	"arimelody-web/model"
)

type loginData struct {
    DiscordURI string
    Token string
}

func Handler() http.Handler {
    mux := http.NewServeMux()

    mux.Handle("/login", LoginHandler())
    mux.Handle("/logout", MustAuthorise(LogoutHandler()))
    mux.Handle("/static/", http.StripPrefix("/static", staticHandler()))
    mux.Handle("/release/", MustAuthorise(http.StripPrefix("/release", serveRelease())))
    mux.Handle("/artist/", MustAuthorise(http.StripPrefix("/artist", serveArtist())))
    mux.Handle("/track/", MustAuthorise(http.StripPrefix("/track", serveTrack())))
    mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path != "/" {
            http.NotFound(w, r)
            return
        }

        session := GetSession(r)
        if session == nil {
            http.Redirect(w, r, "/admin/login", http.StatusFound)
            return
        }

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

        artists, err := controller.GetAllArtists(global.DB)
        if err != nil {
            fmt.Printf("FATAL: Failed to pull artists: %s\n", err)
            http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
            return
        }

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

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

        err = pages["index"].Execute(w, IndexData{
            Releases: releases,
            Artists: artists,
            Tracks: tracks,
        })
		if err != nil {
			fmt.Printf("Error executing template: %s\n", err)
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
			return
		}
    }))

    return mux
}

func MustAuthorise(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        session := GetSession(r)
        if session == nil {
            http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
            return
        }

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

func GetSession(r *http.Request) *Session {
    if ADMIN_BYPASS {
        return &Session{}
    }

    var token = ""
    // is the session token in context?
    var ctx_session = r.Context().Value("session")
    if ctx_session != nil {
        token = ctx_session.(*Session).Token
    }

    // okay, is it in the auth header?
    if token == "" {
        if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
            token = r.Header.Get("Authorization")[7:]
        }
    }
    // finally, is it in the cookie?
    if token == "" {
        cookie, err := r.Cookie("token")
        if err != nil {
            return nil
        }
        token = cookie.Value
    }

    var session *Session = nil
    for _, s := range sessions {
        if s.Expires.Before(time.Now()) {
            // expired session. remove it from the list!
            new_sessions := []*Session{}
            for _, ns := range sessions {
                if ns.Token == s.Token {
                    continue
                }
                new_sessions = append(new_sessions, ns)
            }
            sessions = new_sessions
            continue
        }

        if s.Token == token {
            session = s
            break
        }
    } 

    return session
}

func LoginHandler() http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !discord.CREDENTIALS_PROVIDED || ADMIN_ID_DISCORD == "" {
            http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable)
            return
        }

        code := r.URL.Query().Get("code")

        if code == "" {
            pages["login"].Execute(w, loginData{DiscordURI: discord.REDIRECT_URI})
            return
        }

        auth_token, err := discord.GetOAuthTokenFromCode(code)
        if err != nil {
            fmt.Printf("Failed to retrieve discord access token: %s\n", err)
            http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
            return
        }

        discord_user, err := discord.GetDiscordUserFromAuth(auth_token)
        if err != nil {
            fmt.Printf("Failed to retrieve discord user information: %s\n", err)
            http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
            return
        }

        if discord_user.ID != ADMIN_ID_DISCORD {
            // TODO: unauthorized user; revoke the token
            fmt.Printf("Unauthorized login attempted: %s\n", discord_user.ID)
            http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
            return
        }

        // login success!
        session := createSession(discord_user.Username, time.Now().Add(24 * time.Hour))
        sessions = append(sessions, &session)

        cookie := http.Cookie{}
        cookie.Name = "token"
        cookie.Value = session.Token
        cookie.Expires = time.Now().Add(24 * time.Hour)
        if strings.HasPrefix(global.Config.BaseUrl, "https") {
            cookie.Secure = true
        }
        cookie.HttpOnly = true
        cookie.Path = "/"
        http.SetCookie(w, &cookie)

        err = pages["login"].Execute(w, loginData{Token: session.Token})
        if err != nil {
            fmt.Printf("Error rendering admin login page: %s\n", err)
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
            return
        }
    })
}

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

        session := GetSession(r)

        // remove this session from the list
        sessions = func (token string) []*Session {
            new_sessions := []*Session{}
            for _, session := range sessions {
                if session.Token != token {
                    new_sessions = append(new_sessions, session)
                }
            }
            return new_sessions
        }(session.Token)

        err := pages["logout"].Execute(w, nil)
        if err != nil {
            fmt.Printf("Error rendering admin logout page: %s\n", err)
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
            return
        }
    })
}

func staticHandler() http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        info, err := os.Stat(filepath.Join("admin", "static", filepath.Clean(r.URL.Path)))
        // does the file exist?
        if err != nil {
            if os.IsNotExist(err) {
                http.NotFound(w, r)
                return
            }
        }

        // is thjs a directory? (forbidden)
        if info.IsDir() {
            http.NotFound(w, r)
            return
        }

        http.FileServer(http.Dir(filepath.Join("admin", "static"))).ServeHTTP(w, r)
    })
}
