package proxymap

import (
    "crypto/tls"
    "io/ioutil"
    "net"
    "net/http"
    "net/http/httputil"
    "net/url"
    "os"
    "path/filepath"
    "strings"
    "time"

    "acme-reverseproxy/nfs"
    "github.com/sirupsen/logrus"
)

const (
    acmeChallengePrefix = "/.well-known/acme-challenge/"
    acmeWellknownPrefix = "/.well-known/acme-wellknown/"
)

// IsAcmeWellKnownPath reports whether path is an ACME HTTP-01 challenge URL.
func IsAcmeWellKnownPath(path string) bool {
    return strings.HasPrefix(path, acmeChallengePrefix) || strings.HasPrefix(path, acmeWellknownPrefix)
}

func acmeTokenFromPath(path string) (token string, layout string, ok bool) {
    switch {
    case strings.HasPrefix(path, acmeChallengePrefix):
        layout = acmeChallengePrefix
        token = strings.TrimPrefix(path, acmeChallengePrefix)
    case strings.HasPrefix(path, acmeWellknownPrefix):
        layout = acmeWellknownPrefix
        token = strings.TrimPrefix(path, acmeWellknownPrefix)
    default:
        return "", "", false
    }
    if token == "" || strings.Contains(token, "..") {
        return "", "", false
    }
    return token, layout, true
}

func acmeFilePaths(wellKnownDir, token string) []string {
    return []string{
        filepath.Join(wellKnownDir, "acme-challenge", token),
        filepath.Join(wellKnownDir, token),
    }
}

func defaultAcmeWritePath(wellKnownDir, token, layout string) string {
    if layout == acmeWellknownPrefix {
        return filepath.Join(wellKnownDir, token)
    }
    return filepath.Join(wellKnownDir, "acme-challenge", token)
}

// stripHostPort возвращает только имя хоста, если в строке был порт.
func stripHostPort(host string) string {
    h, _, err := net.SplitHostPort(host)
    if err != nil {
        return host
    }
    return h
}

// tlsServerName задаёт SNI для исходящего TLS к бэкенду.
// При подключении по IP многие серверы отклоняют SNI с IP (tls: unrecognized name);
// в этом случае используем публичное имя из ключа маппинга — как правило, оно совпадает с CN/SAN сертификата бэкенда.
func tlsServerName(frontendKey string, u *url.URL) string {
    backend := u.Hostname()
    if ip := net.ParseIP(backend); ip != nil {
        return stripHostPort(frontendKey)
    }
    return backend
}

func ToReverseProxyMap(m map[string]string) (ReverseProxyMap, error) {
    rpm := ReverseProxyMap{}
    for k, v := range m {
        rpURL, err := url.Parse(v)
        if err != nil {
            return nil, err
        }
        proxy := httputil.NewSingleHostReverseProxy(rpURL)

        // Настройка транспорта для поддержки HTTP и HTTPS бэкендов
        if rpURL.Scheme == "https" {
            sn := tlsServerName(k, rpURL)
            logrus.Infof("Configuring HTTPS transport for backend: %s (TLS ServerName/SNI: %s)", rpURL.String(), sn)
            // Создаем транспорт с поддержкой TLS для HTTPS бэкендов
            transport := &http.Transport{
                Proxy: http.ProxyFromEnvironment,
                TLSClientConfig: &tls.Config{
                    ServerName:         sn,
                    InsecureSkipVerify: false,
                },
                // Настройки подключения
                MaxIdleConns:          100,
                MaxIdleConnsPerHost:   100,
                IdleConnTimeout:       90 * time.Second,
                DisableCompression:    true,
                ForceAttemptHTTP2:     true,
            }
            proxy.Transport = transport
        } else {
            logrus.Debugf("Configuring HTTP transport for backend: %s", rpURL.String())
            // Для HTTP используем стандартный транспорт
            transport := &http.Transport{
                Proxy: http.ProxyFromEnvironment,
                // Настройки подключения
                MaxIdleConns:        100,
                MaxIdleConnsPerHost: 100,
                IdleConnTimeout:     90 * time.Second,
            }
            proxy.Transport = transport
        }

        // Настройка директив для правильной работы прокси
        originalDirector := proxy.Director
        proxy.Director = func(req *http.Request) {
            originalDirector(req)
            // httputil.NewSingleHostReverseProxy уже правильно устанавливает Host и URL
            // Логируем проксирование
            logrus.Infof("Proxying request: %s %s -> %s%s (Request Host: %s, Target: %s, Scheme: %s)",
                req.Method, req.URL.Path, rpURL.Scheme, req.URL.Path, req.Host, rpURL.Host, rpURL.Scheme)
        }

        // Обработка ошибок проксирования
        proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
            logrus.Errorf("Reverse proxy error for %s -> %s: %v", r.Host, rpURL.String(), err)
            http.Error(w, "Bad Gateway", http.StatusBadGateway)
        }

        // Логирование успешных ответов
        originalModifyResponse := proxy.ModifyResponse
        proxy.ModifyResponse = func(resp *http.Response) error {
            if originalModifyResponse != nil {
                if err := originalModifyResponse(resp); err != nil {
                    return err
                }
            }
            logrus.Debugf("Proxy response: %s %s -> status %d",
                resp.Request.Method, resp.Request.URL.Path, resp.StatusCode)
            return nil
        }

        rpm[k] = proxy
        logrus.Infof("Configured reverse proxy: %s -> %s (scheme: %s)", k, rpURL.String(), rpURL.Scheme)
    }
    return rpm, nil
}

type ReverseProxyMap map[string]http.Handler

func NewReverseProxiesHandler(rpm ReverseProxyMap) ReverseProxiesHandler {
    return &reverseProxiesHandler{
        Map:             rpm,
        NotFoundHandler: http.NotFoundHandler(),
        WellKnownDir:    "/tmp/.well-known",
    }
}

func NewReverseProxiesHandlerWithWellKnown(rpm ReverseProxyMap, wellKnownDir string) ReverseProxiesHandler {
    return &reverseProxiesHandler{
        Map:             rpm,
        NotFoundHandler: http.NotFoundHandler(),
        WellKnownDir:    wellKnownDir,
    }
}

type ReverseProxiesHandler interface {
    http.Handler
}

type reverseProxiesHandler struct {
    Map             ReverseProxyMap
    NotFoundHandler http.Handler
    WellKnownDir    string
}

func (rph reverseProxiesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    logrus.Infof("Incoming request: %s %s from %s (Host: %s)", r.Method, r.URL.Path, r.RemoteAddr, r.Host)

    if strings.HasPrefix(r.URL.Path, acmeChallengePrefix) || strings.HasPrefix(r.URL.Path, acmeWellknownPrefix) {
        token, layout, ok := acmeTokenFromPath(r.URL.Path)
        if !ok {
            logrus.Warnf("Invalid .well-known path: %s", r.URL.Path)
            http.Error(w, "Invalid path", http.StatusBadRequest)
            return
        }
        logrus.Infof("Handling ACME well-known request: %s (layout=%s)", r.URL.Path, layout)
        rph.handleWellKnown(w, r, token, layout)
        return
    }

    host, _, err := net.SplitHostPort(r.Host)
    if err != nil {
        if addrErr, ok := err.(*net.AddrError); ok && !strings.Contains(addrErr.Err, "missing port") {
            logrus.Errorf("Error splitting host/port: %T %#v", err, err)
            http.Error(w, "internal error", http.StatusInternalServerError)
            return
        }
        logrus.Debugf("Using %q as hostname (no port)", r.Host)
        host = r.Host
    }

    logrus.Infof("Proxying request: Host=%s, Path=%s, ResolvedHost=%s", r.Host, r.URL.Path, host)

    // Try exact match first
    if v, ok := rph.Map[r.Host]; ok {
        logrus.Infof("Found exact match for host %s, forwarding to backend", r.Host)
        v.ServeHTTP(w, r)
        return
    }

    // Try with resolved host (without port)
    if v, ok := rph.Map[host]; ok {
        logrus.Infof("Found match for resolved host %s, forwarding to backend", host)
        v.ServeHTTP(w, r)
        return
    }

    logrus.Warnf("No mapping found for host: %s (resolved: %s). Available hosts: %v",
        r.Host, host, rph.getHostList())

    if rph.NotFoundHandler == nil {
        http.NotFoundHandler().ServeHTTP(w, r)
        return
    }
    rph.NotFoundHandler.ServeHTTP(w, r)
}

func (rph reverseProxiesHandler) getHostList() []string {
    hosts := make([]string, 0, len(rph.Map))
    for host := range rph.Map {
        hosts = append(hosts, host)
    }
    return hosts
}

func (rph reverseProxiesHandler) handleWellKnown(w http.ResponseWriter, r *http.Request, token, layout string) {
    // Проверяем и при необходимости монтируем NFS перед обработкой
    if err := nfs.EnsureMounted(); err != nil {
        logrus.Errorf("Failed to ensure NFS is mounted: %v", err)
        http.Error(w, "NFS mount error", http.StatusInternalServerError)
        return
    }

    paths := acmeFilePaths(rph.WellKnownDir, token)
    logrus.Infof("Well-known request: method=%s, token=%s, layout=%s, paths=%v",
        r.Method, token, layout, paths)
    switch r.Method {
    case "GET", "HEAD":
        data, _, err := readFirstExisting(paths)
        if err != nil {
            if os.IsNotExist(err) {
                http.NotFound(w, r)
                return
            }
            logrus.Errorf("Error reading ACME challenge files %v: %v", paths, err)
            http.Error(w, "Internal server error", http.StatusInternalServerError)
            return
        }
        w.Header().Set("Content-Type", "application/octet-stream")
        w.WriteHeader(http.StatusOK)
        if r.Method == "GET" {
            w.Write(data)
        }
    case "POST", "PUT":
        data, err := ioutil.ReadAll(r.Body)
        if err != nil {
            logrus.Errorf("Error reading request body: %v", err)
            http.Error(w, "Bad request", http.StatusBadRequest)
            return
        }
        writePath := existingAcmePath(paths)
        if writePath == "" {
            writePath = defaultAcmeWritePath(rph.WellKnownDir, token, layout)
        }
        dir := filepath.Dir(writePath)
        if err := os.MkdirAll(dir, 0755); err != nil {
            logrus.Errorf("Error creating directory %s: %v", dir, err)
            http.Error(w, "Internal server error", http.StatusInternalServerError)
            return
        }
        if err := ioutil.WriteFile(writePath, data, 0644); err != nil {
            logrus.Errorf("Error writing file %s: %v", writePath, err)
            http.Error(w, "Internal server error", http.StatusInternalServerError)
            return
        }
        w.WriteHeader(http.StatusOK)
        if r.Method == "PUT" {
            w.Write([]byte("File created/updated successfully"))
        } else {
            w.Write([]byte("File created successfully"))
        }
    case "DELETE":
        if err := removeFirstExisting(paths); err != nil {
            if os.IsNotExist(err) {
                http.NotFound(w, r)
                return
            }
            logrus.Errorf("Error deleting ACME challenge files %v: %v", paths, err)
            http.Error(w, "Internal server error", http.StatusInternalServerError)
            return
        }
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("File deleted successfully"))
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

func readFirstExisting(paths []string) ([]byte, string, error) {
    var lastErr error
    for _, path := range paths {
        data, err := ioutil.ReadFile(path)
        if err == nil {
            return data, path, nil
        }
        if !os.IsNotExist(err) {
            return nil, "", err
        }
        lastErr = err
    }
    if lastErr == nil {
        lastErr = os.ErrNotExist
    }
    return nil, "", lastErr
}

func existingAcmePath(paths []string) string {
    for _, path := range paths {
        if _, err := os.Stat(path); err == nil {
            return path
        }
    }
    return ""
}

func removeFirstExisting(paths []string) error {
    var lastErr error
    for _, path := range paths {
        err := os.Remove(path)
        if err == nil {
            return nil
        }
        if !os.IsNotExist(err) {
            return err
        }
        lastErr = err
    }
    if lastErr == nil {
        lastErr = os.ErrNotExist
    }
    return lastErr
}
