import { find } from 'lodash';

import { type AzureCredentials } from '@grafana/azure-sdk';
import { type ScopedVars } from '@grafana/data';
import { DataSourceWithBackend, getTemplateSrv, type TemplateSrv } from '@grafana/runtime';

import { getCredentials } from '../credentials';
import { type AzureMetricQuery, AzureQueryType } from '../dataquery.gen';
import TimegrainConverter from '../time_grain_converter';
import { type AzureMonitorQuery } from '../types/query';
import {
  type AzureAPIResponse,
  type AzureMonitorDataSourceInstanceSettings,
  type AzureMonitorDataSourceJsonData,
  type AzureMonitorLocations,
  type AzureMonitorMetricsMetadataResponse,
  type AzureMonitorProvidersResponse,
  type DatasourceValidationResult,
  type GetLogAnalyticsTableResponse,
  type GetMetricMetadataQuery,
  type GetMetricNamespacesQuery,
  type GetMetricNamesQuery,
  instanceOfLogAnalyticsTableError,
  type Location,
  type Metric,
  type MetricNamespace,
  type Subscription,
  TablePlan,
} from '../types/types';
import { replaceTemplateVariables, routeNames } from '../utils/common';
import migrateQuery from '../utils/migrateQuery';

import ResponseParser from './response_parser';
import UrlBuilder from './url_builder';

const defaultDropdownValue = 'select';

function hasValue(item?: string) {
  return !!(item && item !== defaultDropdownValue);
}

export default class AzureMonitorDatasource extends DataSourceWithBackend<
  AzureMonitorQuery,
  AzureMonitorDataSourceJsonData
> {
  private readonly credentials: AzureCredentials;
  apiVersion = '2018-01-01';
  apiPreviewVersion = '2017-12-01-preview';
  listByResourceGroupApiVersion = '2021-04-01';
  providerApiVersion = '2021-04-01';
  locationsApiVersion = '2020-01-01';
  defaultSubscriptionId?: string;
  basicLogsEnabled?: boolean;
  resourcePath: string;
  declare resourceGroup: string;
  declare resourceName: string;

  constructor(
    instanceSettings: AzureMonitorDataSourceInstanceSettings,
    private readonly templateSrv: TemplateSrv = getTemplateSrv()
  ) {
    super(instanceSettings);
    this.credentials = getCredentials(instanceSettings);

    this.defaultSubscriptionId = instanceSettings.jsonData.subscriptionId;
    this.basicLogsEnabled = instanceSettings.jsonData.basicLogsEnabled;

    this.resourcePath = routeNames.azureMonitor;
  }

  isConfigured(): boolean {
    // If validation didn't return any error then the data source is properly configured
    return !this.validateDatasource();
  }

  filterQuery(item: AzureMonitorQuery): boolean {
    const hasResource =
      item?.azureMonitor?.resources &&
      item.azureMonitor.resources.length > 0 &&
      item.azureMonitor.resources.every((r) => hasValue(r.resourceGroup) && hasValue(r.resourceName)) &&
      hasValue(item?.azureMonitor?.metricDefinition || item?.azureMonitor?.metricNamespace);
    const hasResourceUri = hasValue(item.azureMonitor?.resourceUri);

    return !!(
      item.hide !== true &&
      (hasResource || hasResourceUri) &&
      hasValue(item?.azureMonitor?.metricName) &&
      hasValue(item?.azureMonitor?.aggregation)
    );
  }

  applyTemplateVariables(target: AzureMonitorQuery, scopedVars: ScopedVars): AzureMonitorQuery {
    const preMigrationQuery = target.azureMonitor;

    if (!preMigrationQuery) {
      throw new Error('Query is not a valid Azure Monitor Metrics query');
    }

    // These properties need to be replaced pre-migration to ensure values are correctly interpolated
    if (preMigrationQuery.resourceUri) {
      preMigrationQuery.resourceUri = this.templateSrv.replace(preMigrationQuery.resourceUri, scopedVars);
    }
    if (preMigrationQuery.metricDefinition) {
      preMigrationQuery.metricDefinition = this.templateSrv.replace(preMigrationQuery.metricDefinition, scopedVars);
    }

    // fix for timeGrainUnit which is a deprecated/removed field name
    if (preMigrationQuery.timeGrain && preMigrationQuery.timeGrainUnit && preMigrationQuery.timeGrain !== 'auto') {
      preMigrationQuery.timeGrain = TimegrainConverter.createISO8601Duration(
        preMigrationQuery.timeGrain,
        preMigrationQuery.timeGrainUnit
      );
    }

    const migratedTarget = migrateQuery(target);
    const migratedQuery = migratedTarget.azureMonitor;
    // This should never be triggered because the above error would've been thrown
    if (!migratedQuery) {
      throw new Error('Query is not a valid Azure Monitor Metrics query');
    }

    const subscriptionId = this.templateSrv.replace(
      migratedTarget.subscription || this.defaultSubscriptionId,
      scopedVars
    );
    const resources = migratedQuery.resources
      ?.map((r) => replaceTemplateVariables(this.templateSrv, r, scopedVars))
      .flat();
    const metricNamespace = this.templateSrv.replace(migratedQuery.metricNamespace, scopedVars);
    const customNamespace = this.templateSrv.replace(migratedQuery.customNamespace, scopedVars);
    const timeGrain = this.templateSrv.replace((migratedQuery.timeGrain || '').toString(), scopedVars);
    const aggregation = this.templateSrv.replace(migratedQuery.aggregation, scopedVars);
    const top = this.templateSrv.replace(migratedQuery.top || '', scopedVars);

    const dimensionFilters = (migratedQuery.dimensionFilters ?? [])
      .filter((f) => f.dimension && f.dimension !== 'None')
      .map((f) => {
        const filters = f.filters?.map((filter) => this.templateSrv.replace(filter ?? '', scopedVars));
        return {
          dimension: this.templateSrv.replace(f.dimension, scopedVars),
          operator: f.operator || 'eq',
          filters: filters || [],
        };
      });

    const azMonitorQuery: AzureMetricQuery = {
      ...migratedQuery,
      resources,
      metricNamespace,
      customNamespace,
      timeGrain,
      allowedTimeGrainsMs: migratedQuery.allowedTimeGrainsMs,
      metricName: this.templateSrv.replace(migratedQuery.metricName, scopedVars),
      region: this.templateSrv.replace(migratedQuery.region, scopedVars),
      aggregation: aggregation,
      dimensionFilters,
      top: top || '10',
      alias: migratedQuery.alias,
    };

    return {
      ...target,
      subscription: subscriptionId,
      queryType: AzureQueryType.AzureMonitor,
      azureMonitor: azMonitorQuery,
    };
  }

  async getSubscriptions(): Promise<Array<{ text: string; value: string }>> {
    if (!this.isConfigured()) {
      return [];
    }

    return this.getResource<AzureAPIResponse<Subscription>>(
      `${this.resourcePath}/subscriptions?api-version=2019-03-01`
    ).then((result) => {
      return ResponseParser.parseSubscriptions(result);
    });
  }

  // Note globalRegion should be false when querying custom metric namespaces
  getMetricNamespaces(query: GetMetricNamespacesQuery, globalRegion: boolean, region?: string, custom?: boolean) {
    const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
      this.resourcePath,
      this.apiPreviewVersion,
      // Only use the first query, as the metric namespaces should be the same for all queries
      this.replaceSingleTemplateVariables(query),
      globalRegion,
      this.templateSrv,
      region
    );
    return this.getResource(url)
      .then((result: AzureAPIResponse<MetricNamespace>) => {
        if (custom) {
          result.value = result.value.filter((namespace) => namespace.classification === 'Custom');
        }
        return ResponseParser.parseResponseValues(
          result,
          'properties.metricNamespaceName',
          'properties.metricNamespaceName'
        );
      })
      .then((result) => {
        if (url.toLowerCase().includes('microsoft.storage/storageaccounts')) {
          const storageNamespaces = [
            'microsoft.storage/storageaccounts',
            'microsoft.storage/storageaccounts/blobservices',
            'microsoft.storage/storageaccounts/fileservices',
            'microsoft.storage/storageaccounts/tableservices',
            'microsoft.storage/storageaccounts/queueservices',
          ];
          for (const namespace of storageNamespaces) {
            if (!find(result, ['value', namespace.toLowerCase()])) {
              result.push({ value: namespace, text: namespace });
            }
          }
        }
        return result;
      })
      .catch((reason) => {
        console.error(`Failed to get metric namespaces: ${reason}`);
        return [];
      });
  }

  getMetricNames(query: GetMetricNamesQuery, multipleResources?: boolean, region?: string) {
    const apiVersion = multipleResources ? this.apiPreviewVersion : this.apiVersion;
    const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
      this.resourcePath,
      apiVersion,
      // Only use the first query, as the metric names should be the same for all queries
      this.replaceSingleTemplateVariables(query),
      this.templateSrv,
      multipleResources,
      region
    );
    return this.getResource(url).then((result: AzureAPIResponse<Metric>) => {
      return ResponseParser.parseResponseValues(result, 'name.localizedValue', 'name.value');
    });
  }

  getMetricMetadata(query: GetMetricMetadataQuery, multipleResources?: boolean, region?: string) {
    const { metricName } = query;
    const apiVersion = multipleResources ? this.apiPreviewVersion : this.apiVersion;
    const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
      this.resourcePath,
      apiVersion,
      // Only use the first query, as the metric metadata should be the same for all queries
      this.replaceSingleTemplateVariables(query),
      this.templateSrv,
      multipleResources,
      region
    );
    return this.getResource(url).then((result: AzureMonitorMetricsMetadataResponse) => {
      return ResponseParser.parseMetadata(result, this.templateSrv.replace(metricName));
    });
  }

  private validateDatasource(): DatasourceValidationResult | undefined {
    if (this.credentials.authType === 'clientsecret') {
      if (!this.isValidConfigField(this.credentials.tenantId)) {
        return {
          status: 'error',
          message: 'The Tenant Id field is required.',
        };
      }

      if (!this.isValidConfigField(this.credentials.clientId)) {
        return {
          status: 'error',
          message: 'The Client Id field is required.',
        };
      }
    }

    return undefined;
  }

  async getWorkspaceTablePlan(resources: string[], tableName: string): Promise<TablePlan> {
    let workspaceUri = '';

    if (resources) {
      workspaceUri = resources[0];
    }

    if (!workspaceUri) {
      return TablePlan.Analytics;
    }

    if (workspaceUri && !workspaceUri.toLowerCase().includes('microsoft.operationalinsights/workspaces')) {
      // Not a Log Analytics workspace so default to Analytics
      return TablePlan.Analytics;
    }

    const url = UrlBuilder.buildAzureMonitorGetLogsTableUrl(
      this.resourcePath,
      this.templateSrv.replace(workspaceUri),
      this.templateSrv.replace(tableName)
    );
    const tableResult = await this.getResource<GetLogAnalyticsTableResponse>(url);

    if (!tableResult || instanceOfLogAnalyticsTableError(tableResult)) {
      return TablePlan.Analytics;
    }

    return tableResult.properties.plan || TablePlan.Analytics;
  }

  private isValidConfigField(field?: string): boolean {
    return typeof field === 'string' && field.length > 0;
  }

  private replaceSingleTemplateVariables<T extends { [K in keyof T]: string }>(query: T, scopedVars?: ScopedVars) {
    // This method evaluates template variables supporting multiple values but only returns the first value.
    // This will work as far as the first combination of variables is valid.
    // For example if 'rg1' contains 'res1' and 'rg2' contains 'res2' then
    // { resourceGroup: ['rg1', 'rg2'], resourceName: ['res1', 'res2'] } would return
    // { resourceGroup: 'rg1', resourceName: 'res1' } which is valid but
    // { resourceGroup: ['rg1', 'rg2'], resourceName: ['res2'] } would result in
    // { resourceGroup: 'rg1', resourceName: 'res2' } which is not.
    return replaceTemplateVariables(this.templateSrv, query, scopedVars)[0];
  }

  async getProvider(providerName: string) {
    return await this.getResource<AzureMonitorProvidersResponse>(
      `${routeNames.azureMonitor}/providers/${providerName}?api-version=${this.providerApiVersion}`
    );
  }

  async getLocations(subscriptions: string[]) {
    const locationMap = new Map<string, AzureMonitorLocations>();
    for (const subscription of subscriptions) {
      const subLocations = ResponseParser.parseLocations(
        await this.getResource<AzureAPIResponse<Location>>(
          `${routeNames.azureMonitor}/subscriptions/${this.templateSrv.replace(subscription)}/locations?api-version=${
            this.locationsApiVersion
          }`
        )
      );
      for (const location of subLocations) {
        locationMap.set(location.name, location);
      }
    }

    return locationMap;
  }
}
