package services

import (
	"bufio"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"

	"strconv"
	"strings"
	"time"

	"github.com/Wikid82/charon/backend/internal/config"
	"github.com/Wikid82/charon/backend/internal/logger"
	"github.com/Wikid82/charon/backend/internal/models"
)

type LogService struct {
	LogDir string
}

func NewLogService(cfg *config.Config) *LogService {
	// Assuming logs are in data/logs relative to app root
	logDir := filepath.Join(filepath.Dir(cfg.DatabasePath), "logs")
	return &LogService{LogDir: logDir}
}

type LogFile struct {
	Name    string `json:"name"`
	Size    int64  `json:"size"`
	ModTime string `json:"mod_time"`
}

func (s *LogService) ListLogs() ([]LogFile, error) {
	entries, err := os.ReadDir(s.LogDir)
	if err != nil {
		// If directory doesn't exist, return empty list instead of error
		if os.IsNotExist(err) {
			return []LogFile{}, nil
		}
		return nil, err
	}

	var logs []LogFile
	seen := make(map[string]bool)
	for _, entry := range entries {
		hasLogExtension := strings.HasSuffix(entry.Name(), ".log") || strings.Contains(entry.Name(), ".log.")
		if entry.IsDir() || !hasLogExtension {
			continue
		}

		info, err := entry.Info()
		if err != nil {
			continue
		}
		// Handle symlinks + deduplicate files (e.g., charon.log and cpmp.log (legacy name) pointing to same file)
		entryPath := filepath.Join(s.LogDir, entry.Name())
		resolved, err := filepath.EvalSymlinks(entryPath)
		if err == nil {
			if seen[resolved] {
				continue
			}
			seen[resolved] = true
		}
		logs = append(logs, LogFile{
			Name:    entry.Name(),
			Size:    info.Size(),
			ModTime: info.ModTime().Format(time.RFC3339),
		})
	}
	return logs, nil
}

// GetLogPath returns the absolute path to a log file if it exists and is valid
func (s *LogService) GetLogPath(filename string) (string, error) {
	cleanName := filepath.Base(filename)
	if filename != cleanName {
		return "", fmt.Errorf("invalid filename: path traversal attempt detected")
	}
	path := filepath.Join(s.LogDir, cleanName)
	if !strings.HasPrefix(path, filepath.Clean(s.LogDir)) {
		return "", fmt.Errorf("invalid filename: path traversal attempt detected")
	}

	// Verify file exists
	if _, err := os.Stat(path); err != nil {
		return "", err
	}

	return path, nil
}

// QueryLogs parses and filters logs from a specific file
func (s *LogService) QueryLogs(filename string, filter models.LogFilter) ([]models.CaddyAccessLog, int64, error) {
	path, err := s.GetLogPath(filename)
	if err != nil {
		return nil, 0, err
	}

	file, err := os.Open(path)
	if err != nil {
		return nil, 0, err
	}
	defer func() {
		if err := file.Close(); err != nil {
			logger.Log().WithError(err).Warn("failed to close log file after reading")
		}
	}()

	var logs []models.CaddyAccessLog
	var totalMatches int64

	// Read file line by line
	// TODO: For large files, reading from end or indexing would be better
	// Current implementation reads all lines, filters, then paginates
	// This is acceptable for rotated logs (max 10MB)
	scanner := bufio.NewScanner(file)

	// We'll store all matching logs first, then slice for pagination
	// This is memory intensive for very large matches but ensures correct sorting/filtering
	// Since we want latest first, we'll prepend or reverse later.
	// Actually, appending and then reversing is better.

	for scanner.Scan() {
		line := scanner.Text()
		if line == "" {
			continue
		}

		var entry models.CaddyAccessLog
		if err := json.Unmarshal([]byte(line), &entry); err != nil {
			// Handle non-JSON logs (like cpmp.log, legacy name for Charon)
			// Try to parse standard Go log format: "2006/01/02 15:04:05 msg"
			parts := strings.SplitN(line, " ", 3)
			entry.Msg = line
			entry.Level = "INFO" // Default level for plain logs
			if len(parts) >= 3 {
				// Try parsing date/time; if parsing fails, keep the original line as the Msg
				if ts, perr := time.Parse("2006/01/02 15:04:05", parts[0]+" "+parts[1]); perr == nil {
					entry.Ts = float64(ts.Unix())
					entry.Msg = parts[2]
				}
			}
		}

		if s.matchesFilter(entry, filter) {
			logs = append(logs, entry)
		}
	}

	if err := scanner.Err(); err != nil {
		return nil, 0, err
	}

	// Reverse logs to show newest first (default) unless sort is asc
	if filter.Sort != "asc" {
		for i, j := 0, len(logs)-1; i < j; i, j = i+1, j-1 {
			logs[i], logs[j] = logs[j], logs[i]
		}
	}

	totalMatches = int64(len(logs))

	// Apply pagination
	start := filter.Offset
	end := start + filter.Limit

	if start >= len(logs) {
		return []models.CaddyAccessLog{}, totalMatches, nil
	}
	if end > len(logs) {
		end = len(logs)
	}

	return logs[start:end], totalMatches, nil
}

func (s *LogService) matchesFilter(entry models.CaddyAccessLog, filter models.LogFilter) bool {
	// Status Filter
	if filter.Status != "" {
		statusStr := strconv.Itoa(entry.Status)
		if strings.HasSuffix(filter.Status, "xx") {
			// Handle 2xx, 4xx, 5xx
			prefix := filter.Status[:1]
			if !strings.HasPrefix(statusStr, prefix) {
				return false
			}
		} else if statusStr != filter.Status {
			return false
		}
	}

	// Level Filter
	if filter.Level != "" {
		if !strings.EqualFold(entry.Level, filter.Level) {
			return false
		}
	}

	// Host Filter
	if filter.Host != "" {
		if !strings.Contains(strings.ToLower(entry.Request.Host), strings.ToLower(filter.Host)) {
			return false
		}
	}

	// Search Filter (generic text search)
	if filter.Search != "" {
		term := strings.ToLower(filter.Search)
		// Search in common fields
		if !strings.Contains(strings.ToLower(entry.Request.URI), term) &&
			!strings.Contains(strings.ToLower(entry.Request.Method), term) &&
			!strings.Contains(strings.ToLower(entry.Request.RemoteIP), term) &&
			!strings.Contains(strings.ToLower(entry.Msg), term) {
			return false
		}
	}

	return true
}
