import {Injectable} from '@angular/core';
import {HttpClient, HttpErrorResponse, HttpHeaders} from '@angular/common/http';
import {from, Observable, Subject, throwError} from 'rxjs';
import {catchError, finalize, takeUntil, tap} from 'rxjs/operators';
import {Store} from '@ngrx/store';
import {ActionTypes, StateInterface} from '../reducers';
import {ChatbotMessage} from './chatbot.message.type';

/**
 * Interface for Open-Meteo API response
 */
export interface OpenMeteoResponse {
  latitude: number;
  longitude: number;
  generationtime_ms: number;
  utc_offset_seconds: number;
  timezone: string;
  timezone_abbreviation: string;
  elevation: number;
  hourly_units: {
    time: string;
    temperature_2m: string;
  };
  hourly: {
    time: string[];
    temperature_2m: number[];
  };
}

/**
 * gérer la connexion avec le backend
 */
@Injectable({
  providedIn: 'root'
})
export class ApiService {

  /**
   * /api/v1
   *  /messages
   *  /{userId}/conversations
   *  /{conversationId}/feedback
   *  /conversations/{conversationId}
   *  /conversations/{conversationId}/last-answer
   *  /send-documents
   *  /file // lien pour récupérer un doc via s3
   *
   */

  private BASE_URL = "";
  // Ollama API URL - default to localhost:11434
  private OLLAMA_API_URL = "http://localhost:11434";
  // Default Ollama model to use
  // private OLLAMA_MODEL = "llava";
  // private OLLAMA_MODEL = "mistral:7b";
  private OLLAMA_MODEL = "tinyllama:latest";

  private ENDPOINTS: any = {
    "main": {
      "v1": {
        "messages": "messages",
        "user": {
          "conversations": "/{userId}/conversations",
          "feedback": "/user-feedback"
        },
// /{userId}/conversations
// /{conversationId}/feedback
// /conversations/{conversationId}
// /conversations/{conversationId}/last-answer
// /send-documents
// /file
        "conversations": {
          "id": "/{conversationId}",
          "feedback": "/{conversationId}/feedback",
          "last-anwser": "/{conversationId}/last-answer",
        },
        "send-documents": "send-documents",
        "file": "file",

      }
    }
  };
  private CUSTOM_HEADERS: any = {
    "method.response.header.Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
    "method.response.header.Access-Control-Allow-Methods": "OPTIONS,POST",
    "method.response.header.Access-Control-Allow-Origin": "'*'",
  }

  constructor(private http: HttpClient, private store: Store<StateInterface>) {
  }

  async fetchLogin() {
    console.log("fetchLogin");
    return new Promise<any>((resolve, reject) => {
    });
  }

  async fetchEndPoint(kind: string = '', subKind: string = ''): Promise<Observable<Object>> {
    // Record the start time for measuring response time
    const startTime = Date.now();

    let fetchUrl: string = `${this.BASE_URL}/api/v1`
    if (null !== this.ENDPOINTS.main.v1[kind]) {
      fetchUrl += this.ENDPOINTS.main.v1[kind]
      if (null !== this.ENDPOINTS.main.v1[kind][subKind]) {
        fetchUrl += '/' + this.ENDPOINTS.main.v1[kind][subKind]
      }
    }

    // Get the response and pipe it to measure response time
    const resp: Observable<Object> = await this.http.get(fetchUrl,
      {headers: this.CUSTOM_HEADERS}
    ).pipe(
      tap(() => {
        // Measure and update the response time
        this.measureResponseTime(startTime);
      })
    );

    return resp;
  }

  /**
   * Fetches weather forecast data from the Open-Meteo API
   *
   * @returns Observable with the typed JSON response from Open-Meteo API
   *
   * Example usage:
   * ```
   * this.apiService.fetchDemoResponse().subscribe(
   *   (data) => {
   *     console.log('Weather data:', data);
   *     console.log('Temperature at noon:', data.hourly.temperature_2m[12]);
   *   },
   *   (error) => console.error('Error fetching weather data:', error)
   * );
   * ```
   */
  fetchDemoResponse(): Observable<OpenMeteoResponse> {
    // Coordinates for Corbeil-Essonne
    const latitude = "48.57727";
    const longitude = "4.42634";

    const url = 'https://api.open-meteo.com/v1/forecast';

    // Record the start time for measuring response time
    const startTime = Date.now();

    // Get the current average response time for the delay
    return new Observable<OpenMeteoResponse>(observer => {
      this.store.select(state => state.app.averageResponseTime).pipe(
        tap(averageResponseTime => {
          // Use the average response time for the delay
          setTimeout(() => {
            this.http.get<OpenMeteoResponse>(url, {
              params: {
                latitude,
                longitude,
                hourly: 'temperature_2m',
                forecast_days: '1'
              }
            }).pipe(
              tap(response => {
                console.log('Open-Meteo API Response:', response);
                // Measure and update the response time
                this.measureResponseTime(startTime);
              }),
              catchError(this.handleError)
            ).subscribe({
              next: (response) => {
                observer.next(response);
                observer.complete();
              },
              error: (error) => {
                observer.error(error);
              }
            });
          }, averageResponseTime); // Use the average response time for the delay
        })
      ).subscribe().unsubscribe(); // Unsubscribe after getting the value
    });
  }

  /**
   * Send user feedback to the API
   * @param feedback The feedback text from the user
   * @returns Observable with the API response
   */
  async sendUserFeedback(feedback: string): Promise<Observable<Object>> {
    // Record the start time for measuring response time
    const startTime = Date.now();

    const fetchUrl: string = `${this.BASE_URL}/api/v1/user-feedback`;

    // Get the response and pipe it to measure response time
    const resp: Observable<Object> = await this.http.post(
      fetchUrl,
      {feedback},
      {headers: this.CUSTOM_HEADERS}
    ).pipe(
      tap(() => {
        // Measure and update the response time
        this.measureResponseTime(startTime);
      })
    );

    return resp;
  }

  /**
   * Check if the Ollama backend is available
   * @returns Observable that emits true if Ollama is available, false otherwise
   */
  checkOllamaAvailability(): Observable<boolean> {
    return new Observable<boolean>(observer => {
      // Make a simple request to the Ollama API to check if it's available
      this.http.get(`${this.OLLAMA_API_URL}/api/tags`, {
        headers: new HttpHeaders({
          'Content-Type': 'application/json'
        }),
        observe: 'response'
      }).pipe(
        catchError(error => {
          console.error('Error checking Ollama availability:', error);
          // Update the Redux store to indicate Ollama is not available
          this.store.dispatch({
            type: ActionTypes.UPDATE_APP,
            payload: {
              ollamaAvailable: false
            }
          });
          // Return false to indicate Ollama is not available
          return from([false]);
        })
      ).subscribe({
        next: (response) => {
          // If we get a response, Ollama is available
          // Check if response is an HttpResponse (has status property)
          const isAvailable = typeof response === 'object' && response !== null && 'status' in response && response.status === 200;

          // Update the Redux store with the result
          this.store.dispatch({
            type: ActionTypes.UPDATE_APP,
            payload: {
              ollamaAvailable: isAvailable
            }
          });

          // Emit the result and complete the observable
          observer.next(isAvailable);
          observer.complete();
        },
        error: () => {
          // If there's an error, Ollama is not available
          observer.next(false);
          observer.complete();
        }
      });
    });
  }

  /**
   * Query the Ollama backend with a prompt and handle streaming responses
   * @param prompt The text prompt to send to Ollama
   * @returns Observable that completes when the full response is received
   */
  queryOllama(prompt: string): Observable<void> {
    // Create a subject to notify when to complete the stream
    const complete$ = new Subject<void>();

    // Record the start time for measuring response time
    const startTime = Date.now();

    // Set loading state to true
    this.store.dispatch({
      type: ActionTypes.UPDATE_APP,
      payload: {
        loading: true
      }
    });

    // Create an empty response to accumulate the streamed content
    let accumulatedResponse = '';

    // Create the request payload
    const payload = {
      model: this.OLLAMA_MODEL,
      prompt: 'réponds en Français en étant un assistant amical et respectueux. Ta réponse doit être consiste steuplai. Et fais des retours à la ligne dans tes réponses. Voici ma question : ' + prompt,
      stream: false,
      // raw: true,
    };

    // Set up headers for the request
    const headers = new HttpHeaders({
      'Content-Type': 'application/json'
    });

    // Create a subject that will emit the final response
    const result = new Subject<void>();

    // Make the request to Ollama API
    this.http.post(`${this.OLLAMA_API_URL}/api/generate`, payload, {
      headers,
      responseType: 'json',
      observe: 'response'
    })
      .pipe(
        takeUntil(complete$),
        finalize(() => {

          this.store.dispatch({
            type: ActionTypes.UPDATE_APP,
            payload: {
              loading: false
            }
          });

          this.measureResponseTime(startTime);

          // If we have accumulated a response, create a message and add it to the conversation
          if (accumulatedResponse) {

            accumulatedResponse = accumulatedResponse.replace(/\. /g, ". <br/><br/>");
            const responseMessage = new ChatbotMessage({
              kind: 'llm',
              user: {},
              content: accumulatedResponse,
              name: 'Ollama'
            });

            // Dispatch the message to the active conversation
            this.store.dispatch({
              type: ActionTypes.POST_MESSAGE_TO_ACTIVE_CONVERSATION,
              payload: {
                message: responseMessage
              }
            });

            // Get the current active conversation from the store
            this.store.select(state => state.activeConversation).subscribe(activeConv => {
              if (activeConv && Object.keys(activeConv).length > 0) {
                // Update the conversation in the conversations list
                this.store.dispatch({
                  type: ActionTypes.UPDATE_SINGLE_CONVERSATION,
                  payload: {
                    conversation: activeConv
                  }
                });
              }
            }).unsubscribe(); // Unsubscribe immediately after getting the value
          }

          // Complete the result subject
          result.complete();
        })
      )
      .subscribe({
        next: (response) => {
          console.log('Ollama response:', response);
          if (response.body) {
            // Process the non-streaming response
            // Ollama returns a single JSON object with a 'response' field
            try {
              const responseData = response.body as any;
              if (responseData.response) {
                // Set the accumulated response text
                accumulatedResponse = responseData.response;
                console.log('Ollama response:', accumulatedResponse);

                // Complete the stream
                complete$.next();
                complete$.complete();
              } else {
                console.error('No response field found in Ollama response:', responseData);
              }
            } catch (e) {
              console.error('Error processing Ollama response:', e);
            }
          }
        },
        error: (error) => {
          console.error('Error querying Ollama:', error);

          // Set loading state to false
          this.store.dispatch({
            type: ActionTypes.UPDATE_APP,
            payload: {
              loading: false,
              displayErrorResponse: true
            }
          });

          // Send error message to chat
          const errorMessage = new ChatbotMessage({
            kind: 'llm',
            user: {},
            content: "Error connecting to Ollama backend. Please check if the service is running.",
            name: 'System'
          });

          this.store.dispatch({
            type: ActionTypes.POST_MESSAGE_TO_ACTIVE_CONVERSATION,
            payload: {
              message: errorMessage
            }
          });

          // Get the current active conversation from the store
          this.store.select(state => state.activeConversation).subscribe(activeConv => {
            if (activeConv && Object.keys(activeConv).length > 0) {
              // Update the conversation in the conversations list
              this.store.dispatch({
                type: ActionTypes.UPDATE_SINGLE_CONVERSATION,
                payload: {
                  conversation: activeConv
                }
              });
            }
          }).unsubscribe(); // Unsubscribe immediately after getting the value

          // Reset error state after 3 seconds
          setTimeout(() => {
            this.store.dispatch({
              type: ActionTypes.UPDATE_APP,
              payload: {
                displayErrorResponse: false
              }
            });
          }, 3000);

          // Complete the subjects
          complete$.next();
          complete$.complete();
          result.error(error);
        }
      });

    return result.asObservable();
  }

  /**
   * Updates the average response time in the store
   * @param responseTime The actual response time in milliseconds
   */
  private updateAverageResponseTime(responseTime: number): void {
    // Get the current average response time from the store
    this.store.select(state => state.app.averageResponseTime).subscribe(currentAverage => {
      // Calculate the new average (average of current average and new response time)
      const newAverage = Math.round((currentAverage + responseTime) / 2);

      // Update the store with the new average
      this.store.dispatch({
        type: ActionTypes.UPDATE_APP,
        payload: {
          averageResponseTime: newAverage
        }
      });
    }).unsubscribe(); // Unsubscribe immediately after getting the value
  }

  /**
   * Measures the response time of an API call and updates the average
   * @param startTime The timestamp when the API call started
   */
  private measureResponseTime(startTime: number): void {
    const endTime = Date.now();
    const responseTime = endTime - startTime;
    this.updateAverageResponseTime(responseTime);
  }

  /**
   * Error handler for HTTP requests
   * @param error The HTTP error response
   * @returns An observable that errors with a user-friendly message
   */
  private handleError(error: HttpErrorResponse): Observable<never> {
    let errorMessage = 'An unknown error occurred';

    if (error.error instanceof ErrorEvent) {
      // Client-side error
      errorMessage = `Error: ${error.error.message}`;
    } else {
      // Server-side error
      errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
    }

    console.error(errorMessage);
    return throwError(() => new Error(errorMessage));
  }
}
