package handler

import (
	"encoding/json"
	"net/http"

	"gopkg.in/yaml.v3"
)

// openAPISpec is the embedded OpenAPI 3.0 specification for the proxy API.
var openAPISpec = `
openapi: "3.0.3"
info:
  title: Claude Code Proxy API
  description: |
    An Anthropic API proxy that provides request logging, model routing, usage
    analytics, and a dashboard UI.  The proxy exposes two groups of endpoints:

    **Proxy endpoints** – drop-in replacements for the upstream Anthropic API.
    Point your Claude Code (or any Anthropic SDK client) at this proxy and all
    requests are forwarded, logged, and optionally re-routed to a different model
    or provider.

    **Dashboard endpoints** – read-only analytics and configuration APIs that
    power the built-in web dashboard.  These are protected by HTTP Basic Auth
    when DASHBOARD_PASSWORD is set.
  version: "1.0.0"
  contact:
    name: Claude Code Proxy
  license:
    name: MIT

servers:
  - url: /
    description: This proxy instance

tags:
  - name: proxy
    description: |
      Drop-in Anthropic API proxy endpoints.  Authenticate with the same
      x-api-key / Authorization header you use for the upstream Anthropic API.
  - name: dashboard
    description: |
      Analytics and configuration endpoints for the web dashboard.
      Protected by DASHBOARD_PASSWORD basic auth when configured.
  - name: health
    description: Health and discovery endpoints (no auth required).

paths:
  # ── Proxy endpoints ────────────────────────────────────────────────────
  /v1/messages:
    post:
      operationId: createMessage
      tags: [proxy]
      summary: Create a message (Anthropic Messages API)
      description: |
        Forwards the request to the upstream Anthropic (or routed) provider.
        Supports both streaming (SSE) and non-streaming responses.  The proxy
        logs the request/response, applies any configured model routing rules
        and header rules, then returns the upstream response verbatim.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AnthropicRequest"
      responses:
        "200":
          description: Successful message response (non-streaming)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AnthropicResponse"
            text/event-stream:
              schema:
                type: string
                description: SSE stream of Anthropic streaming events
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Upstream or internal error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/chat/completions:
    post:
      operationId: chatCompletions
      tags: [proxy]
      summary: Chat completions (OpenAI-compatible – not supported)
      description: |
        Returns a 400 error directing callers to use /v1/messages instead.
        This endpoint exists for compatibility detection only.
      requestBody:
        content:
          application/json:
            schema:
              type: object
      responses:
        "400":
          description: Not supported – use /v1/messages
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/models:
    get:
      operationId: listModels
      tags: [proxy]
      summary: List available models
      description: |
        Returns the list of models known to the proxy.  The proxy uses
        pattern-based routing so any model accepted by the upstream provider
        will work; this endpoint currently returns an empty list.
      responses:
        "200":
          description: Model list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ModelsResponse"

  # ── Health & discovery ─────────────────────────────────────────────────
  /health:
    get:
      operationId: healthCheck
      tags: [health]
      summary: Health check (binary up/ready signal for load balancers)
      description: |
        Returns 200 with status=healthy while the process is accepting
        traffic, and 503 with status=draining once a SIGTERM has been
        received. Traefik (or any LB doing health-based routing) should
        treat 503 as "stop sending new requests to this backend", which is
        the signal the graceful-drain loop relies on.
      responses:
        "200":
          description: Service is healthy
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
        "503":
          description: Service is draining (SIGTERM received). Stop routing here.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DrainingResponse"

  /livez:
    get:
      operationId: livenessProbe
      tags: [health]
      summary: Live operational state (in-flight gauge + draining flag)
      description: |
        Always returns 200 with the current in-flight request count and
        draining flag. Distinct from /health, which is a binary up/ready
        signal — /livez is for observability and deploy-time orchestration
        ("how many requests are still active before I cycle this slot?").
      responses:
        "200":
          description: Operational state
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LivezResponse"

  /openapi.json:
    get:
      operationId: getOpenAPISpec
      tags: [health]
      summary: OpenAPI specification (JSON)
      responses:
        "200":
          description: The OpenAPI 3.0 spec for this API
          content:
            application/json:
              schema:
                type: object

  /openapi.yaml:
    get:
      operationId: getOpenAPISpecYAML
      tags: [health]
      summary: OpenAPI specification (YAML)
      responses:
        "200":
          description: The OpenAPI 3.0 spec for this API
          content:
            application/x-yaml:
              schema:
                type: string

  # ── Dashboard endpoints ────────────────────────────────────────────────
  /api/requests:
    get:
      operationId: getRequests
      tags: [dashboard]
      summary: List logged requests
      parameters:
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: limit
          in: query
          schema: { type: integer, default: 10 }
        - name: model
          in: query
          schema: { type: string, default: "all" }
          description: Filter by model name (substring match) or "all"
      responses:
        "200":
          description: Paginated request list
          content:
            application/json:
              schema:
                type: object
                properties:
                  requests:
                    type: array
                    items:
                      $ref: "#/components/schemas/RequestLog"
                  total:
                    type: integer
    delete:
      operationId: deleteRequests
      tags: [dashboard]
      summary: Clear all logged requests
      responses:
        "200":
          description: Requests cleared
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
                  deleted: { type: integer }

  /api/requests/summary:
    get:
      operationId: getRequestsSummary
      tags: [dashboard]
      summary: Lightweight request summaries for fast list rendering
      parameters:
        - name: model
          in: query
          schema: { type: string, default: "all" }
        - name: start
          in: query
          schema: { type: string, format: date-time }
          description: Start of time range (UTC ISO 8601)
        - name: end
          in: query
          schema: { type: string, format: date-time }
          description: End of time range (UTC ISO 8601)
        - name: offset
          in: query
          schema: { type: integer, default: 0 }
        - name: limit
          in: query
          schema: { type: integer, default: 0 }
          description: Max results (0 = unlimited)
      responses:
        "200":
          description: Paginated request summaries
          content:
            application/json:
              schema:
                type: object
                properties:
                  requests:
                    type: array
                    items:
                      $ref: "#/components/schemas/RequestSummary"
                  total: { type: integer }
                  offset: { type: integer }
                  limit: { type: integer }

  /api/requests/latest-date:
    get:
      operationId: getLatestRequestDate
      tags: [dashboard]
      summary: Date of the most recent logged request
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  latestDate: { type: string, format: date-time }

  /api/requests/{id}:
    get:
      operationId: getRequestByID
      tags: [dashboard]
      summary: Get a single request by ID
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
          description: Short or full request ID
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  request:
                    $ref: "#/components/schemas/RequestLog"
                  fullId: { type: string }
        "404":
          description: Request not found

  /api/stats:
    get:
      operationId: getStats
      tags: [dashboard]
      summary: Aggregated usage statistics
      parameters:
        - name: start_date
          in: query
          schema: { type: string }
        - name: end_date
          in: query
          schema: { type: string }
        - name: model
          in: query
          schema: { type: string }
        - name: org
          in: query
          schema: { type: string }
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UsageStats"

  /api/stats/dashboard:
    get:
      operationId: getDashboardStats
      tags: [dashboard]
      summary: Daily token usage for dashboard charts
      parameters:
        - name: start
          in: query
          schema: { type: string, format: date-time }
        - name: end
          in: query
          schema: { type: string, format: date-time }
        - name: org
          in: query
          schema: { type: string }
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardStats"

  /api/stats/hourly:
    get:
      operationId: getHourlyStats
      tags: [dashboard]
      summary: Hourly token usage breakdown
      parameters:
        - name: start
          in: query
          required: true
          schema: { type: string, format: date-time }
        - name: end
          in: query
          required: true
          schema: { type: string, format: date-time }
        - name: bucket
          in: query
          schema: { type: integer, default: 60 }
          description: Bucket size in minutes
        - name: org
          in: query
          schema: { type: string }
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HourlyStatsResponse"

  /api/stats/models:
    get:
      operationId: getModelStats
      tags: [dashboard]
      summary: Per-model token usage breakdown
      parameters:
        - name: start
          in: query
          required: true
          schema: { type: string, format: date-time }
        - name: end
          in: query
          required: true
          schema: { type: string, format: date-time }
        - name: org
          in: query
          schema: { type: string }
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ModelStatsResponse"

  /api/stats/organizations:
    get:
      operationId: getOrganizations
      tags: [dashboard]
      summary: List distinct organization IDs
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  organizations:
                    type: array
                    items: { type: string }

  /api/conversations:
    get:
      operationId: getConversations
      tags: [dashboard]
      summary: List conversations (grouped by session)
      parameters:
        - name: model
          in: query
          schema: { type: string, default: "all" }
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: limit
          in: query
          schema: { type: integer, default: 10 }
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  conversations:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        requestCount: { type: integer }
                        startTime: { type: string, format: date-time }
                        lastActivity: { type: string, format: date-time }
                        duration: { type: integer, description: "Duration in ms" }
                        firstMessage: { type: string }
                        projectPath: { type: string }
                        projectName: { type: string }
                        model: { type: string }
                  hasMore: { type: boolean }
                  total: { type: integer }
                  page: { type: integer }
                  limit: { type: integer }

  /api/conversations/{id}:
    get:
      operationId: getConversationByID
      tags: [dashboard]
      summary: Get a single conversation by session ID
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: project
          in: query
          required: true
          schema: { type: string }
          description: Project path the conversation belongs to
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
        "404":
          description: Conversation not found

  /api/conversations/project:
    get:
      operationId: getConversationsByProject
      tags: [dashboard]
      summary: List conversations for a specific project
      parameters:
        - name: project
          in: query
          required: true
          schema: { type: string }
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object

  /api/settings:
    get:
      operationId: getSettings
      tags: [dashboard]
      summary: Get current proxy settings
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProxySettings"
    put:
      operationId: saveSettings
      tags: [dashboard]
      summary: Update proxy settings
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProxySettings"
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProxySettings"

components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: x-api-key
      description: Anthropic API key (forwarded to upstream)
    bearerAuth:
      type: http
      scheme: bearer
      description: Bearer token authentication
    dashboardBasicAuth:
      type: http
      scheme: basic
      description: Dashboard password (username is ignored)

  schemas:
    ErrorResponse:
      type: object
      properties:
        error: { type: string }
        details: { type: string }

    HealthResponse:
      type: object
      properties:
        status: { type: string, example: "healthy" }
        timestamp: { type: string, format: date-time }

    DrainingResponse:
      type: object
      properties:
        status: { type: string, example: "draining" }
        timestamp: { type: string, format: date-time }
        in_flight: { type: integer, example: 3 }

    LivezResponse:
      type: object
      properties:
        status: { type: string, example: "ok" }
        timestamp: { type: string, format: date-time }
        in_flight: { type: integer, example: 0 }
        draining: { type: boolean, example: false }

    AnthropicRequest:
      type: object
      required: [model, messages, max_tokens]
      properties:
        model:
          type: string
          description: |
            Model ID to use.  The proxy may re-route this to a different
            model/provider based on configured routing rules.
          example: "claude-sonnet-4-5-20250514"
        messages:
          type: array
          items:
            $ref: "#/components/schemas/AnthropicMessage"
        max_tokens:
          type: integer
          example: 1024
        temperature:
          type: number
          format: float
        system:
          type: array
          items:
            $ref: "#/components/schemas/SystemMessage"
        stream:
          type: boolean
          default: false
        tools:
          type: array
          items:
            $ref: "#/components/schemas/Tool"
        tool_choice:
          description: Tool choice configuration

    AnthropicMessage:
      type: object
      required: [role, content]
      properties:
        role:
          type: string
          enum: [user, assistant]
        content:
          description: String or array of content blocks
          oneOf:
            - type: string
            - type: array
              items:
                type: object
                properties:
                  type: { type: string }
                  text: { type: string }

    SystemMessage:
      type: object
      properties:
        type: { type: string, example: "text" }
        text: { type: string }
        cache_control:
          type: object
          properties:
            type: { type: string, example: "ephemeral" }

    Tool:
      type: object
      properties:
        name: { type: string }
        description: { type: string }
        input_schema:
          type: object
          properties:
            type: {}
            properties: { type: object }
            required:
              type: array
              items: { type: string }

    AnthropicResponse:
      type: object
      properties:
        id: { type: string }
        type: { type: string, example: "message" }
        role: { type: string, example: "assistant" }
        model: { type: string }
        stop_reason: { type: string }
        stop_sequence: { type: string, nullable: true }
        content:
          type: array
          items:
            type: object
            properties:
              type: { type: string }
              text: { type: string }
        usage:
          $ref: "#/components/schemas/AnthropicUsage"

    AnthropicUsage:
      type: object
      properties:
        input_tokens: { type: integer }
        output_tokens: { type: integer }
        cache_creation_input_tokens: { type: integer }
        cache_read_input_tokens: { type: integer }
        service_tier: { type: string }

    ModelsResponse:
      type: object
      properties:
        object: { type: string, example: "list" }
        data:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              object: { type: string }
              created: { type: integer }
              owned_by: { type: string }

    RequestLog:
      type: object
      properties:
        requestId: { type: string }
        timestamp: { type: string, format: date-time }
        method: { type: string }
        endpoint: { type: string }
        model: { type: string }
        originalModel: { type: string }
        routedModel: { type: string }
        userAgent: { type: string }
        contentType: { type: string }
        conversationHash: { type: string }
        messageCount: { type: integer }
        organizationId: { type: string }
        response:
          $ref: "#/components/schemas/ResponseLog"

    ResponseLog:
      type: object
      properties:
        statusCode: { type: integer }
        responseTime: { type: integer, description: "Response time in ms" }
        isStreaming: { type: boolean }
        completedAt: { type: string, format: date-time }
        streamError: { type: string }
        rateLimit:
          $ref: "#/components/schemas/RateLimitInfo"

    RateLimitInfo:
      type: object
      properties:
        organizationId: { type: string }
        requestsLimit: { type: integer }
        requestsRemaining: { type: integer }
        requestsReset: { type: string }
        tokensLimit: { type: integer }
        tokensRemaining: { type: integer }
        tokensReset: { type: string }
        unifiedStatus: { type: string }
        unifiedUtilization5h: { type: number }
        unifiedReset5h: { type: string }
        unifiedUtilization7d: { type: number }
        unifiedReset7d: { type: string }

    RequestSummary:
      type: object
      properties:
        requestId: { type: string }
        timestamp: { type: string, format: date-time }
        method: { type: string }
        endpoint: { type: string }
        model: { type: string }
        originalModel: { type: string }
        routedModel: { type: string }
        statusCode: { type: integer }
        responseTime: { type: integer }
        usage:
          $ref: "#/components/schemas/AnthropicUsage"
        conversationHash: { type: string }
        messageCount: { type: integer }
        stopReason: { type: string }

    UsageStats:
      type: object
      properties:
        total_requests: { type: integer }
        total_input_tokens: { type: integer, format: int64 }
        total_output_tokens: { type: integer, format: int64 }
        total_cache_tokens: { type: integer, format: int64 }
        requests_by_model:
          type: object
          additionalProperties:
            type: object
            properties:
              request_count: { type: integer }
              input_tokens: { type: integer, format: int64 }
              output_tokens: { type: integer, format: int64 }
              cache_tokens: { type: integer, format: int64 }
        start_date: { type: string }
        end_date: { type: string }

    DashboardStats:
      type: object
      properties:
        dailyStats:
          type: array
          items:
            type: object
            properties:
              date: { type: string }
              tokens: { type: integer, format: int64 }
              requests: { type: integer }

    HourlyStatsResponse:
      type: object
      properties:
        hourlyStats:
          type: array
          items:
            type: object
            properties:
              hour: { type: integer }
              label: { type: string }
              tokens: { type: integer, format: int64 }
              requests: { type: integer }
        todayTokens: { type: integer, format: int64 }
        todayRequests: { type: integer }
        avgResponseTime: { type: integer, format: int64 }

    ModelStatsResponse:
      type: object
      properties:
        modelStats:
          type: array
          items:
            type: object
            properties:
              model: { type: string }
              tokens: { type: integer, format: int64 }
              requests: { type: integer }

    ProxySettings:
      type: object
      properties:
        requestHeaderRules:
          type: array
          items:
            $ref: "#/components/schemas/HeaderRule"
        responseHeaderRules:
          type: array
          items:
            $ref: "#/components/schemas/HeaderRule"

    HeaderRule:
      type: object
      properties:
        header: { type: string, description: "Header name (case-insensitive)" }
        action:
          type: string
          enum: [block, set, replace]
        value: { type: string }
        find: { type: string, description: "For replace action: string to find" }
        enabled: { type: boolean }

security:
  - apiKey: []
  - bearerAuth: []
`

// OpenAPIJSON serves the OpenAPI spec as JSON.
func (h *Handler) OpenAPIJSON(w http.ResponseWriter, r *http.Request) {
	var spec interface{}
	if err := yaml.Unmarshal([]byte(openAPISpec), &spec); err != nil {
		writeErrorResponse(w, "Failed to parse OpenAPI spec", http.StatusInternalServerError)
		return
	}
	spec = convertYAMLToJSON(spec)
	w.Header().Set("Content-Type", "application/json")
	w.Header().Set("Access-Control-Allow-Origin", "*")
	json.NewEncoder(w).Encode(spec)
}

// OpenAPIYAML serves the OpenAPI spec as YAML.
func (h *Handler) OpenAPIYAML(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/x-yaml")
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Write([]byte(openAPISpec))
}

// convertYAMLToJSON recursively converts map[string]interface{} (from yaml) to
// JSON-compatible types. yaml.v3 uses map[string]interface{} by default so this
// mainly handles nested maps.
func convertYAMLToJSON(v interface{}) interface{} {
	switch val := v.(type) {
	case map[string]interface{}:
		out := make(map[string]interface{}, len(val))
		for k, v2 := range val {
			out[k] = convertYAMLToJSON(v2)
		}
		return out
	case []interface{}:
		out := make([]interface{}, len(val))
		for i, v2 := range val {
			out[i] = convertYAMLToJSON(v2)
		}
		return out
	default:
		return v
	}
}
