/*
 * Copyright (C) 2022 StApps
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation, version 3.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <https://www.gnu.org/licenses/>.
 */
import {Injectable} from '@angular/core';
import {Client} from '@openstapps/api';
import {SCAppConfiguration, SCIndexResponse} from '@openstapps/core';
import {CORE_VERSION} from '@openstapps/core';
import {NGXLogger} from 'ngx-logger';
import {environment} from '../../../environments/environment';
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
import {StorageProvider} from '../storage/storage.provider';
import {
  ConfigFetchError,
  ConfigInitError,
  ConfigValueNotAvailable,
  SavedConfigNotAvailable,
  WrongConfigVersionInStorage,
} from './errors';
import {InternetConnectionService} from '../../util/internet-connection.service';
import {firstValueFrom} from 'rxjs';

/**
 * Key to store config in storage module
 *
 * TODO: Issue #41 centralise storage keys
 */
export const STORAGE_KEY_CONFIG = 'stapps.config';

/**
 * Provides configuration
 */
@Injectable({
  providedIn: 'root',
})
export class ConfigProvider {
  /**
   * Api client
   */
  client: Client;

  /**
   * App configuration as IndexResponse
   */
  config: SCIndexResponse;

  /**
   * Version of the @openstapps/core package that app is using
   */
  scVersion = CORE_VERSION;

  /**
   * First session indicator (config not found in storage)
   */
  firstSession = true;

  /**
   * Constructor, initialise api client
   * @param storageProvider StorageProvider to load persistent configuration
   * @param swHttpClient Api client
   * @param logger An angular logger
   */
  constructor(
    private readonly storageProvider: StorageProvider,
    swHttpClient: StAppsWebHttpClient,
    private readonly logger: NGXLogger,
    private readonly internetConnectionService: InternetConnectionService,
  ) {
    console.log('config init');
    this.client = new Client(swHttpClient, environment.backend_url, environment.backend_version);
  }

  /**
   * Fetches configuration from backend
   */
  async fetch(): Promise<void> {
    try {
      const isOffline = await firstValueFrom(this.internetConnectionService.offline$);
      if (isOffline) {
        throw new Error('Device is offline.');
      } else {
        const fetchedConfig: SCIndexResponse = await this.client.handshake(this.scVersion);
        await this.set(fetchedConfig);
        this.logger.log(`Configuration updated from remote`);
      }
    } catch (error) {
      const error_ = error instanceof Error ? new ConfigFetchError(error.message) : new ConfigFetchError();
      this.logger.warn(`Failed to fetch remote configuration:`, error_);
      throw error_; // Rethrow the error to handle it in init()
    }
  }

  /**
   * Returns the value of an app configuration
   * @param attribute requested attribute from app configuration
   */
  public getValue(attribute: keyof SCAppConfiguration) {
    if (this.config.app[attribute] !== undefined) {
      return this.config.app[attribute];
    }
    throw new ConfigValueNotAvailable(attribute);
  }

  /**
   * Returns a value of the configuration (not only app configuration)
   * @param attribute requested attribute from the configuration
   */
  public getAnyValue(attribute: keyof SCIndexResponse) {
    if (this.config[attribute] !== undefined) {
      return this.config[attribute];
    }
    throw new ConfigValueNotAvailable(attribute);
  }

  /**
   * Initialises the ConfigProvider
   * @throws ConfigInitError if no configuration could be loaded both locally and remote.
   */
  async init(): Promise<void> {
    try {
      // Attempt to load the configuration from local storage
      this.config = await this.loadLocal();
      this.firstSession = false;
      this.logger.log(`initialised configuration from storage`);

      // Check if the stored configuration has the correct version
      if (this.config.backend.SCVersion.split('.')[0] !== this.scVersion.split('.')[0]) {
        throw new WrongConfigVersionInStorage(this.scVersion, this.config.backend.SCVersion);
      }

      // Fetch the remote configuration in a non-blocking manner
      void this.fetch();
    } catch (loadError) {
      this.logger.warn(loadError);

      try {
        // If local loading fails, immediately try to fetch the configuration from remote
        await this.fetch();
      } catch (fetchError) {
        this.logger.warn(`Failed to fetch remote configuration:`, fetchError);
        // If both local loading and remote fetching fail, throw ConfigInitError
        throw new ConfigInitError();
      }
    }
  }

  /**
   * Returns saved configuration from StorageModule
   * @throws SavedConfigNotAvailable if no configuration could be loaded
   */
  async loadLocal(): Promise<SCIndexResponse> {
    // get local configuration
    if (await this.storageProvider.has(STORAGE_KEY_CONFIG)) {
      return this.storageProvider.get<SCIndexResponse>(STORAGE_KEY_CONFIG);
    }
    throw new SavedConfigNotAvailable();
  }

  /**
   * Saves the configuration from the provider
   * @param config configuration to save
   */
  async save(config: SCIndexResponse): Promise<void> {
    await this.storageProvider.put(STORAGE_KEY_CONFIG, config);
  }

  /**
   * Sets the configuration in the module and writes it into app storage
   * @param config SCIndexResponse to set
   */
  async set(config: SCIndexResponse): Promise<void> {
    this.config = config;
    await this.save(this.config);
  }
}
