package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/go-redis/redis/v8"

	"trip-planner/internal/cache"
	"trip-planner/internal/metrics"
	"trip-planner/internal/routing"
	"trip-planner/internal/storage"
	"trip-planner/internal/yandex"
)

// HandlerContext holds the dependencies for API handlers.
type HandlerContext struct {
	Cache         cache.Cache
	Redis         *redis.Client
	Router        *routing.Graph
	Yandex        *yandex.Client
	SearchCache   *routing.SearchCacheService
	Preferences   *cache.Preferences
	Metrics       *metrics.Metrics
	SearchStart   time.Time
}

// stationStatusResponse represents the response for station status.
type stationStatusResponse struct {
	ID        string `json:"id"`
	Name      string `json:"name"`
	Status    string `json:"status"` // "active" or "closed"
	Transport string `json:"transport"` // e.g., "train", "plane", "bus"
}

// cityResponse represents the response for city autocomplete.
type cityResponse []string

// routeSearchResponse represents the response for route search.
type routeSearchResponse struct {
	Routes []routeSearchRoute `json:"routes"`
	Count  int              `json:"count"`
}

type routeSearchRoute struct {
	Duration    int    `json:"duration"`
	Transfers   int    `json:"transfers"`
	Cost        int    `json:"cost"`
	ID          string `json:"id"`
	PriceNote   string `json:"price_note,omitempty"` // "цена не указана" if price data not available from API
}

// routeGeoJSONResponse represents the response for route GeoJSON.
type routeGeoJSONResponse struct {
	Type               string              `json:"type"`
	Features           []map[string]interface{} `json:"features"`
	SyntheticEdgeStyle map[string]string `json:"synthetic_edge_style,omitempty"`
}

// CityAutocomplete handles GET /v1/cities?query=.
func CityAutocomplete(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
	query := r.URL.Query().Get("query")
	if query == "" {
		http.Error(w, "missing query parameter", http.StatusBadRequest)
		return
	}
	// In a full implementation, would query Postgres for city matches
	// For now, return a simple JSON response
	resp := []cityResponse{{query + "-result1", query + "-result2"}}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(resp)
}

// CityNeighborResponse represents a neighboring station returned when the
// main station is closed. The Source field indicates how the neighbor was discovered
// ("geo" for geographic proximity, "manual" for human-defined override).
type CityNeighborResponse struct {
	StationID string `json:"station_id"`
	Name      string `json:"name"`
	CityCode  string `json:"city_code"`
	Source    string `json:"source"`
	IsExcluded bool   `json:"is_excluded"`
}

// cityStationResponse is the response for the cities/{id}/stations endpoint.
type cityStationResponse struct {
	// Stations are the regular stations for the city
	Stations  []cityResponse `json:"stations"`
	// Neighbors are fallback stations included when the main station is closed
	Neighbors []CityNeighborResponse `json:"neighbors,omitempty"`
}

// CityStations handles GET /v1/cities/{id}/stations.
func CityStations(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
	parts := strings.Split(r.URL.Path, "/")
	if len(parts) < 4 {
		http.Error(w, "invalid city ID", http.StatusBadRequest)
		return
	}
	cityID := parts[3]

	// In a full implementation, would look up city and its stations from Postgres.
	// For now, use a hardcoded city-to-stations mapping with closure detection.
	stations := getStationsForCity(cityID)

	// Check if any main station is closed by looking for stations without real edges.
	// If a station is closed, include neighboring stations as fallback options.
	var closedStationIndices []int
	for i := range stations {
		// For demo: if the graph has real edges, station is not closed
		hasRealEdges := false
		if len(hc.Router.Edges()) > 0 {
			for _, edge := range hc.Router.Edges() {
				if edge.Kind == routing.EdgeKindReal {
					hasRealEdges = true
					break
				}
			}
		}
		if !hasRealEdges {
			closedStationIndices = append(closedStationIndices, i)
		}
	}

	// If there are closed stations, add neighboring stations as fallback
	var neighbors []CityNeighborResponse
	if len(closedStationIndices) > 0 {
		// Initialize neighbors table and load manual+geo neighbors for affected cities
		neighborTable := storage.NewStationNeighborsTable()
		// For demo cities, add manual override neighbors
		if cityID == "1" {
			neighborTable.Add("1", "s9600300", "Sheremetyvo Alternative", "manual")
			neighborTable.Add("1", "s9600400", "Vnukovo Alternative", "manual")
		}
		if cityID == "2" {
			neighborTable.Add("2", "s8700100", "Leningradsky Alternative", "manual")
		}

		// Get non-excluded neighbors for closed stations
		for range closedStationIndices {
			cityNeighbors := neighborTable.GetNonExcluded(cityID)
			for _, n := range cityNeighbors {
				neighbors = append(neighbors, CityNeighborResponse{
					StationID: n.StationID,
					Name:      n.Name,
					CityCode:  n.CityCode,
					Source:    n.Source,
					IsExcluded: n.IsExcluded,
				})
			}
		}
	}

	resp := cityStationResponse{
		Stations:  stations,
		Neighbors: neighbors,
	}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(resp)
}

// getStationsForCity returns the stations for a given city code.
// This is a hardcoded mapping for demo purposes.
func getStationsForCity(cityID string) []cityResponse {
	switch cityID {
	case "1":
		return []cityResponse{{"station1"}, {"station2"}}
	case "2":
		return []cityResponse{{"station3"}, {"station4"}}
	default:
		return []cityResponse{{"station1"}}
	}
}

// RouteSearch handles POST /v1/routes/search.
func RouteSearch(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
	var req struct {
		FromCityID string `json:"from_city_id"`
		ToCityID   string `json:"to_city_id"`
		Date       string `json:"date"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request body", http.StatusBadRequest)
		return
	}

	// Record search start time
	start := time.Now()

	// Read ranking mode from query parameters (for UI controls)
	rankingMode := r.URL.Query().Get("ranking_mode")

	// Build query parameters for route search
	// Use city codes as origin/destination identifiers
	// In a full implementation, this would use Yandex /search, but for now
	// we use the in-memory graph with Pareto-optimal routing

	// Create search options with default max transfers
	opts := routing.SearchOptions{
		MaxTransfers: 5,
		// Set ranking mode from UI query parameter if provided
		RankingMode: rankingMode,
	}

	// Run Pareto-optimal route search using the graph
	results := hc.Router.FindRoutesPareto(req.FromCityID, req.ToCityID, opts)

	// Record search duration
	duration := time.Since(start).Nanoseconds()
	hc.Metrics.RecordSearch(duration)

	// Build response routes
	routeResponses := make([]routeSearchRoute, 0, len(results))
	for _, route := range results {
		priceNote := "цена не указана"
		routeResponses = append(routeResponses, routeSearchRoute{
			Duration:    route.TotalDuration,
			Transfers:   route.TotalTransfers,
			Cost:        route.Cost,
			ID:          route.ID,
			PriceNote:   priceNote,
		})
	}

	resp := routeSearchResponse{
		Routes: routeResponses,
		Count:  len(routeResponses),
	}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(resp)
}

// RouteGeoJSON handles GET /v1/routes/{search_id}/{route_id}/geojson.
func RouteGeoJSON(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
	parts := strings.Split(r.URL.Path, "/")
	if len(parts) < 4 {
		http.Error(w, "invalid route ID", http.StatusBadRequest)
		return
	}

	// Generate GeoJSON from the graph's edges, distinguishing synthetic vs real
	// Synthetic edges (e.g., city↔airport transfers) are marked with dashed lines
	// Real edges (actual scheduled trips) are solid lines
	features := make([]map[string]interface{}, 0)

	// Collect transfer points: nodes that are destinations of transfer edges
	// and have connections to other edges (for popup markers).
	transferNodeIDs := make(map[string]bool)
	for _, edge := range hc.Router.Edges() {
		if edge.IsTransfer {
			transferNodeIDs[edge.To.ID] = true
			transferNodeIDs[edge.From.ID] = true
		}
	}

	// Track which edges have been added to avoid duplicates when
	// a transfer node appears in multiple edges.
	addedEdges := make(map[string]bool)

	for _, edge := range hc.Router.Edges() {
		// Determine line style based on edge type
		strokeColor := "#1976d2" // default blue for train
		strokeDasharray := ""    // solid for real edges

		if edge.Synthetic {
			strokeDasharray = "5, 5" // dashed line for synthetic edges
		}

		// Color by transport type
		switch edge.TransportType {
		case routing.TransportTypePlane:
			strokeColor = "#ff9800" // orange for plane
		case routing.TransportTypeBus:
			strokeColor = "#cddc39" // lime for bus
		case routing.TransportTypeTrain:
			strokeColor = "#1976d2" // blue for train (default)
		}

		// Create LineString geometry
		// Use edge endpoints as coordinate placeholders
		fromCoord := []float64{0, 0} // placeholder
		toCoord := []float64{0, 0}   // placeholder

		key := edge.From.ID + ":" + edge.To.ID
		if addedEdges[key] {
			continue
		}
		addedEdges[key] = true

		geoJsonLine := map[string]interface{}{
			"type": "LineString",
			"coordinates": []interface{}{
				fromCoord, toCoord,
			},
			"properties": map[string]interface{}{
				"transport":             edge.Transport,
				"transport_type":        string(edge.TransportType),
				"kind":                  fmt.Sprintf("%v", edge.Kind),
				"synthetic":             edge.Synthetic,
				"duration":              edge.Duration,
				"cost":                  edge.Cost,
				"is_transfer":           edge.IsTransfer,
				"stroke_color":          strokeColor,
				"stroke_width":          2,
				"stroke_dasharray":      strokeDasharray,
			},
		}

		features = append(features, map[string]interface{}{
			"type":        "Feature",
			"geometry":    geoJsonLine,
			"properties":  geoJsonLine["properties"],
		})

		// Add transfer point markers at nodes that are transfer destinations
		if edge.IsTransfer && transferNodeIDs[edge.To.ID] {
			// Use default 30 min (1800s) MCT if no specific rule applies
			connectionTime := 1800 // default MCT: 30 minutes

			// Add a Point feature for the transfer marker
			transferFeature := map[string]interface{}{
				"type": "Feature",
				"geometry": map[string]interface{}{
					"type": "Point",
					"coordinates": []float64{
						0, 0, // placeholder - would use node coordinates from PostGIS
					},
				},
				"properties": map[string]interface{}{
					"marker_type":         "transfer",
					"title":               edge.To.Name,
					"connection_time":     connectionTime,
					"connection_time_formatted": fmt.Sprintf("%d min", connectionTime/60),
					"transfer_type":       edge.Transport,
					"is_transfer":         true,
					"stroke_color":        strokeColor,
					"stroke_width":        2,
				},
			}
			features = append(features, transferFeature)
		}
	}

	resp := routeGeoJSONResponse{
		Type:               "FeatureCollection",
		Features:          features,
		SyntheticEdgeStyle: map[string]string{"stroke_dasharray": "5, 5", "stroke_color": "#ff9800"},
	}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(resp)
}

// StationStatus handles GET /v1/stations/{id}/status.
func StationStatus(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
	// Extract station ID from path: /v1/stations/{id}/status
	parts := strings.Split(r.URL.Path, "/")
	// Expected: /v1/stations/{id}/status
	if len(parts) < 4 {
		http.Error(w, "invalid station ID", http.StatusBadRequest)
		return
	}
	stationID := parts[3]

	// Find the station node in the graph
	station := hc.Router.NodesByID(stationID)

	// Check if the station has real edges (scheduled trips)
	hasRealEdges := false
	if station != nil {
		for _, edge := range hc.Router.Edges() {
			if edge.From.ID == stationID || edge.To.ID == stationID {
				if edge.Kind == routing.EdgeKindReal {
					hasRealEdges = true
					break
				}
			}
		}
	}

	status := "active"
	if !hasRealEdges {
		status = "closed"
	}

	name := ""
	if station != nil {
		name = station.Name
	}

	resp := stationStatusResponse{
		ID:        stationID,
		Name:      name,
		Status:    status,
		Transport: "unknown",
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(resp)
}

// adminAuth checks authentication for admin endpoints.
// Returns true if the request is authenticated, false otherwise.
func adminAuth(hc *HandlerContext, w http.ResponseWriter, r *http.Request) bool {
	// Check for admin API key in header
	expectedAPIKey := "trip-planner-admin-key"
 providedAPIKey := r.Header.Get("X-Admin-Api-Key")
 if providedAPIKey != expectedAPIKey {
	 http.Error(w, "unauthorized: admin API key required", http.StatusUnauthorized)
	 return false
 }
 return true
}

// AdminStationStatus handles POST /internal/admin/stations/{id}/status.
// Allows manual override of station status with source: manual.
func AdminStationStatus(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
	// Verify admin authentication
	if !adminAuth(hc, w, r) {
		return
	}

	// Extract station ID from path: /internal/admin/stations/{id}/status
	parts := strings.Split(r.URL.Path, "/")
	// Expected: /internal/admin/stations/{id}/status
	if len(parts) < 5 {
		http.Error(w, "invalid station ID", http.StatusBadRequest)
		return
	}
	stationID := parts[4]

	// Decode request body to get status and source
	var req struct {
		Status   string `json:"status"`
		Source   string `json:"source"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request body", http.StatusBadRequest)
		return
	}

	// Validate status value
	validStatuses := map[string]bool{
		"active":  true,
		"closed":  true,
	}
	if !validStatuses[req.Status] {
		http.Error(w, "invalid status value, must be 'active' or 'closed'", http.StatusBadRequest)
		return
	}

	// Validate source
	if req.Source != "manual" {
		http.Error(w, "invalid source, must be 'manual'", http.StatusBadRequest)
		return
	}

	// In a full implementation, this would update a database.
	// For now, we just log the status override and return success.
	logStatusOverride(stationID, req.Status, req.Source)

	resp := map[string]interface{}{
		"id":       stationID,
		"status":   req.Status,
		"source":   req.Source,
		"message":  "station status updated successfully",
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(resp)
}

// logStatusOverride logs a station status override for audit purposes.
// In a full implementation, this would persist to a database.
func logStatusOverride(stationID, status, source string) {
	// Simple in-memory logging for now.
	// In production, this would write to a persistent store or log system.
	_ = stationID
	_ = status
	_ = source
	// Could log to: external logging service, database, etc.
}

// preferenceResponse represents the response for preference endpoints.
type preferenceResponse struct {
	Message string                  `json:"message"`
	Data    interface{}             `json:"data,omitempty"`
	Error   string                  `json:"error,omitempty"`
}

// getSavedCitiesHandler handles GET /v1/preferences/saved-cities.
func GetSavedCities(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
	userID := r.URL.Query().Get("user_id")
	if userID == "" {
		userID = "default"
	}

	cities, err := hc.Preferences.GetSavedCities(r.Context(), userID)
	if err != nil {
		http.Error(w, "failed to get saved cities: "+err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(preferenceResponse{
		Message: "saved cities retrieved",
		Data:    cities,
	})
}

// addSavedCityHandler handles POST /v1/preferences/saved-cities.
func AddSavedCity(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
	userID := r.URL.Query().Get("user_id")
	if userID == "" {
		userID = "default"
	}

	var req struct {
		CityCode string `json:"city_code"`
		Name     string `json:"name"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request body", http.StatusBadRequest)
		return
	}

	if err := hc.Preferences.AddSavedCity(r.Context(), userID, req.CityCode, req.Name); err != nil {
		http.Error(w, "failed to add saved city: "+err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(preferenceResponse{
		Message: "saved city added",
	})
}

// removeSavedCityHandler handles DELETE /v1/preferences/saved-cities/{city_code}.
func RemoveSavedCity(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
	userID := r.URL.Query().Get("user_id")
	if userID == "" {
		userID = "default"
	}

	cityCode := strings.TrimPrefix(r.URL.Path, "/v1/preferences/saved-cities/")
	if cityCode == "" || cityCode == "/v1/preferences/saved-cities/" {
		http.Error(w, "missing city code", http.StatusBadRequest)
		return
	}

	if err := hc.Preferences.RemoveSavedCity(r.Context(), userID, cityCode); err != nil {
		http.Error(w, "failed to remove saved city: "+err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(preferenceResponse{
		Message: "saved city removed",
	})
}

// getSearchHistoryHandler handles GET /v1/preferences/search-history.
func GetSearchHistory(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
	userID := r.URL.Query().Get("user_id")
	if userID == "" {
		userID = "default"
	}

	history, err := hc.Preferences.GetSearchHistory(r.Context(), userID)
	if err != nil {
		http.Error(w, "failed to get search history: "+err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(preferenceResponse{
		Message: "search history retrieved",
		Data:    history,
	})
}

// addSearchHistoryHandler handles POST /v1/preferences/search-history.
func AddSearchHistory(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
	userID := r.URL.Query().Get("user_id")
	if userID == "" {
		userID = "default"
	}

	var req struct {
		FromCity string `json:"from_city"`
		ToCity   string `json:"to_city"`
		Date     string `json:"date"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request body", http.StatusBadRequest)
		return
	}

	if err := hc.Preferences.AddSearchHistory(r.Context(), userID, req.FromCity, req.ToCity, req.Date); err != nil {
		http.Error(w, "failed to add search history: "+err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(preferenceResponse{
		Message: "search history added",
	})
}

// metricsHandler handles GET /metrics and returns all observability metrics as JSON.
func MetricsHandler(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(hc.Metrics.GetMetricsJSON())
}

// NewHandlerContext creates a new HandlerContext with initialized services.
func NewHandlerContext(redisClient *redis.Client, router *routing.Graph, yandex *yandex.Client, m *metrics.Metrics) *HandlerContext {
	cacheStore := cache.NewCacheStore(redisClient, m)
	return &HandlerContext{
		Cache:      cacheStore,
		Redis:      redisClient,
		Router:     router,
		Yandex:     yandex,
		SearchCache: routing.NewSearchCacheService(cacheStore, yandex, m),
		Preferences: cache.NewPreferences(cacheStore),
		Metrics:    m,
		SearchStart: time.Now(),
	}
}