// DataverseService — utility generica per le web resource Dataverse.
// Usa il contesto globale (ClientGlobalContext.js.aspx / Xrm.Utility.getGlobalContext()).
// Generato da Dataverse Helper.

/* eslint-disable @typescript-eslint/no-explicit-any */

export interface ODataCollection<T = any> {
  value: T[];
  "@odata.nextLink"?: string;
}

interface CacheEntry {
  data: any;
  timestamp: number;
}

export class DataverseService {
  private globalContext: any = null;
  private version = "9.2";
  private baseUrl = "";
  private cache = new Map<string, CacheEntry>();
  private cacheTimeout = 5 * 60 * 1000; // 5 minuti

  /** Inizializza con il contesto globale Dataverse. */
  init(globalContext: any): void {
    if (!globalContext) {
      throw new Error("Global context richiesto");
    }
    this.globalContext = globalContext;
    this.version = this.getCurrentVersion(globalContext);
    this.baseUrl = `${globalContext.getClientUrl()}/api/data/v${this.version}`;
  }

  /** Ottiene il contesto globale da Xrm (host SPA o form). */
  static getGlobalContext(): any {
    const w = window as any;
    const xrm = w.Xrm ?? (w.parent && w.parent.Xrm);
    if (xrm?.Utility?.getGlobalContext) {
      return xrm.Utility.getGlobalContext();
    }
    if (typeof w.GetGlobalContext === "function") {
      return w.GetGlobalContext();
    }
    throw new Error("Contesto Dataverse non disponibile");
  }

  private getCurrentVersion(context: any): string {
    if (!context.getVersion) {
      return "9.2";
    }
    const parts = String(context.getVersion()).split(".");
    return parts.length >= 2 ? `${parts[0]}.${parts[1]}` : "9.2";
  }

  removeWhiteSpaceInXml(xml: string): string {
    if (!xml) {
      return "";
    }
    return xml
      .replace(/\n/g, "")
      .replace(/[\t ]+</g, "<")
      .replace(/>[\t ]+</g, "><")
      .replace(/>[\t ]+$/g, ">");
  }

  removeCurlyBraces(id: string): string {
    return id ? id.replace(/[{}]/g, "") : "";
  }

  /** fetch con retry su errori di rete. */
  async makeRequest(
    url: string,
    options: RequestInit = {},
    retries = 3
  ): Promise<Response> {
    try {
      const response = await fetch(url, {
        ...options,
        headers: {
          "OData-MaxVersion": "4.0",
          "OData-Version": "4.0",
          Accept: "application/json",
          "Content-Type": "application/json; charset=utf-8",
          ...(options.headers || {}),
        },
      });
      if (!response.ok) {
        const err = await response.json().catch(() => ({ message: "Unknown error" }));
        throw new Error(
          `HTTP ${response.status}: ${err.error?.message || err.message || "Request failed"}`
        );
      }
      return response;
    } catch (error: any) {
      if (retries > 0 && (error.name === "TypeError" || String(error.message).includes("fetch"))) {
        await new Promise((r) => setTimeout(r, 1000));
        return this.makeRequest(url, options, retries - 1);
      }
      throw error;
    }
  }

  private async withCache<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
    const cached = this.cache.get(key);
    const now = Date.now();
    if (cached && now - cached.timestamp < this.cacheTimeout) {
      return cached.data as T;
    }
    const data = await fetcher();
    this.cache.set(key, { data, timestamp: now });
    return data;
  }

  private prefer(): Record<string, string> {
    return {
      Prefer:
        "odata.maxpagesize=5000, odata.include-annotations=OData.Community.Display.V1.FormattedValue",
    };
  }

  /** RetrieveMultiple via FetchXML. */
  async retrieveMultipleFetchXml<T = any>(
    entitySetName: string,
    fetchXml: string,
    useCache = false
  ): Promise<ODataCollection<T>> {
    const exec = async () => {
      const url = `${this.baseUrl}/${entitySetName}?fetchXml=${encodeURIComponent(
        this.removeWhiteSpaceInXml(fetchXml)
      )}`;
      const res = await this.makeRequest(url, { headers: this.prefer() });
      return (await res.json()) as ODataCollection<T>;
    };
    return useCache
      ? this.withCache(`fetch_${entitySetName}_${fetchXml}`, exec)
      : exec();
  }

  /** RetrieveMultiple via query OData (es. "$select=name&$filter=..."). */
  async retrieveMultiple<T = any>(
    entitySetName: string,
    odataQuery = ""
  ): Promise<ODataCollection<T>> {
    const url = `${this.baseUrl}/${entitySetName}${odataQuery ? `?${odataQuery}` : ""}`;
    const res = await this.makeRequest(url, { headers: this.prefer() });
    return (await res.json()) as ODataCollection<T>;
  }

  /** Crea un record; ritorna la rappresentazione (return=representation). */
  async create<T = any>(
    entitySetName: string,
    data: Record<string, any>,
    select?: string
  ): Promise<T> {
    const url = `${this.baseUrl}/${entitySetName}${select ? `?$select=${select}` : ""}`;
    const res = await this.makeRequest(url, {
      method: "POST",
      headers: { Prefer: "return=representation" },
      body: JSON.stringify(data),
    });
    return (await res.json()) as T;
  }

  async update(
    entitySetName: string,
    id: string,
    data: Record<string, any>
  ): Promise<void> {
    const url = `${this.baseUrl}/${entitySetName}(${this.removeCurlyBraces(id)})`;
    await this.makeRequest(url, { method: "PATCH", body: JSON.stringify(data) });
  }

  async deleteRecord(entitySetName: string, id: string): Promise<void> {
    const url = `${this.baseUrl}/${entitySetName}(${this.removeCurlyBraces(id)})`;
    await this.makeRequest(url, { method: "DELETE" });
  }

  /** Metadati di un OptionSet (Picklist). */
  async getOptionSetMetadata(entityLogicalName: string, attributeLogicalName: string): Promise<any> {
    return this.withCache(`optionset_${entityLogicalName}_${attributeLogicalName}`, async () => {
      const url =
        `${this.baseUrl}/EntityDefinitions(LogicalName='${entityLogicalName}')` +
        `/Attributes(LogicalName='${attributeLogicalName}')` +
        `/Microsoft.Dynamics.CRM.PicklistAttributeMetadata?$select=LogicalName&$expand=OptionSet($select=Options)`;
      const res = await this.makeRequest(url);
      return res.json();
    });
  }

  async whoAmI(): Promise<{ UserId: string; BusinessUnitId: string; OrganizationId: string }> {
    const res = await this.makeRequest(`${this.baseUrl}/WhoAmI()`);
    return res.json();
  }

  async testConnection(): Promise<boolean> {
    try {
      await this.whoAmI();
      return true;
    } catch {
      return false;
    }
  }

  clearCache(): void {
    this.cache.clear();
  }
}

export default DataverseService;
