import SparqlJsonParser from "./json";
import TurtleParser, { getTurtleAsStatements } from "./turtleFamily";
import SparqlXmlParser from "./xml";
import SparqlCsvParser from "./csv";
import SparqlTsvParser from "./tsv";
import bindingsToCsv from "../bindingsToCsv";
import { cloneDeep } from "lodash-es";
import N3 from "n3";

namespace Parser {
  export interface ErrorSummary {
    status?: number;
    text: string;
    statusText?: string;
  }
  export interface BindingValue {
    value: string;
    type: "uri" | "literal" | "typed-literal" | "bnode";
    datatype?: string;
    "xml:lang"?: string;
  }
  export interface Binding {
    [varname: string]: BindingValue;
  }
  export interface SparqlResults {
    head: {
      vars: string[];
    };
    boolean?: boolean;
    results?: {
      bindings: Binding[];
    };
  }
  //a json response summary, that we can store in localstorage
  export interface ResponseSummary {
    data?: any;
    error?: ErrorSummary;
    status?: number;
    contentType?: string;
    executionTime?: number;
  }
  export type PostProcessBinding = (binding: Binding) => Binding;
}
const applyMustacheToLiterals: Parser.PostProcessBinding = (binding: Parser.Binding) => {
  for (const lit in binding) {
    if (binding[lit].type === "uri") continue;
    binding[lit].value = binding[lit].value.replace(/{{(.*?)}}/g, (variable) => {
      variable = variable.substring(2, variable.length - 2).trim();
      if (binding[variable]) {
        return binding[variable].value;
      } else {
        return variable;
      }
    });
  }
  return binding;
};

/**
 * Response object from a fetch request with an additional `.content` property containing the awaited response content
 */
type QueryResponse = Response & {
  content: string;
};

/**
 * Parser class for handling query responses from various formats.
 *
 * This class processes responses from SPARQL endpoints and converts them into standardized formats for further processing.
 * It can handle JSON, XML, CSV, TSV, and Turtle responses, detecting the format either from content-type headers
 * or by attempting to parse the content.
 *
 * The parser can process:
 * - Direct `QueryResponse` objects (regular fetch response with additional content property with the awaited response content)
 * - ResponseSummary objects
 * - Error objects
 * - Raw response data
 *
 * It provides methods to access parsed data in standardized formats, error information,
 * and utilities to convert the data for storage or export.
 */
class Parser {
  private res: QueryResponse | undefined;
  private summary: Parser.ResponseSummary | undefined;
  private errorSummary: Parser.ErrorSummary | undefined;
  private error: Error | undefined;
  private type: "json" | "xml" | "csv" | "tsv" | "ttl" | undefined;
  private executionTime: number | undefined;

  constructor(responseOrObject: Parser.ResponseSummary | QueryResponse | Error | any, executionTime?: number) {
    if (responseOrObject.executionTime) this.executionTime = responseOrObject.executionTime;
    if (executionTime) this.executionTime = executionTime; // Parameter has priority
    if (responseOrObject instanceof Error) {
      this.error = responseOrObject;
    } else if ((<any>responseOrObject).content) {
      this.setResponse(<QueryResponse>responseOrObject);
    } else {
      this.setSummary(<Parser.ResponseSummary>responseOrObject);
    }
  }
  public setResponse(res: QueryResponse) {
    this.res = res;
  }
  public setSummary(summary: Parser.ResponseSummary | any) {
    if (!summary.data && !summary.error) {
      //This isnt a summary object, just try to recreate a summary object ourselves (assuming we're just passed a sparql-result object directly)
      this.summary = {
        data: summary,
      };
    } else {
      this.summary = summary;
    }
  }

  public hasError() {
    if (this.res && this.res.status >= 400) return true;
    if (this.errorSummary) return true;
    if (this.error) return true;
    if (this.summary && this.summary.error) return true;
    return false;
  }
  public getError() {
    if (!this.errorSummary) {
      if (this.res && this.res.status >= 400) {
        this.errorSummary = {
          text: this.res.statusText,
          status: this.res.status,
          statusText: !this.res.ok ? this.res.statusText : undefined,
        };
      }
      if (this.summary && this.summary.error) {
        this.errorSummary = this.summary.error;
      }
      if (this.error) {
        this.errorSummary = {
          text: this.error.message,
        };
      }
    }
    return this.errorSummary;
  }
  public getContentType() {
    if (this.res) return this.res.headers.get("content-type") || undefined;
    if (this.summary) return this.summary.contentType;
  }
  private json: false | Parser.SparqlResults | undefined;
  getAsJson() {
    if (this.json) return this.json;
    if (this.json === false || this.hasError()) return; //already tried parsing this, and failed. do not try again...
    if (this.getParserFromContentType()) return this.json;
    if (this.doLuckyGuess()) return this.json;

    if (!this.json) this.json = false; //explicitly set to false, so we don't try to parse this thing again..
    return this.json;
  }
  private getData(): any {
    if (this.res) {
      if (this.res.content) return this.res.content;
      if (this.res.body) return this.res.body;
      if (this.res.text) return this.res.text; //probably a construct or something
    }
    if (this.summary) return this.summary.data;
  }
  public getResponseTime() {
    return this.executionTime;
  }
  private getParserFromContentType(): boolean {
    const contentType = this.getContentType();
    if (contentType) {
      const data = cloneDeep(this.getData());
      try {
        if (contentType.indexOf("json") > -1) {
          if (contentType.indexOf("sparql-results+json") >= 0) {
            this.json = SparqlJsonParser(data, applyMustacheToLiterals);
            this.type = "json";
            return true;
          } else if (contentType.indexOf("application/rdf+json") > -1) {
            this.type = "json";
            return true;
          }
          this.type = "json";
        } else if (contentType.indexOf("xml") > -1) {
          this.json = SparqlXmlParser(data, applyMustacheToLiterals);
          this.type = "xml";
          return true;
        } else if (contentType.indexOf("csv") > -1) {
          this.json = SparqlCsvParser(data);
          this.type = "csv";
          return true;
        } else if (contentType.indexOf("tab-separated") > -1) {
          this.json = SparqlTsvParser(data);
          this.type = "tsv";
          return true;
        } else if (
          contentType.indexOf("turtle") > 0 ||
          contentType.indexOf("trig") > 0 ||
          contentType.indexOf("triple") > 0 ||
          contentType.indexOf("quad") > 0
        ) {
          this.json = TurtleParser(data);
          this.type = "ttl";
          return true;
        }
      } catch (e) {
        if (e instanceof Error) {
          this.errorSummary = { text: e.message };
        } else {
          this.errorSummary = { text: e as any };
        }
      }
    }
    return false;
  }
  private doLuckyGuess(): boolean {
    const data = cloneDeep(this.getData());
    try {
      this.json = SparqlJsonParser(data, applyMustacheToLiterals);
      this.type = "json";
      return true;
    } catch {}
    try {
      this.json = SparqlXmlParser(data, applyMustacheToLiterals);
      this.type = "xml";
      return true;
    } catch {}
    return false;
  }

  public getVariables(): string[] {
    const json = this.getAsJson();

    if (json && json.head) return json.head.vars;

    return [];
  }

  public getBoolean(): boolean | undefined {
    const json = this.getAsJson();
    if (json && "boolean" in json) return json.boolean;
  }

  public getBindings() {
    const json = this.getAsJson();
    if (json && json.results) return json.results.bindings;
    else return null;
  }
  private statements: false | N3.Quad[] | undefined;
  public getStatements() {
    if (!this.statements && this.type === "ttl") {
      this.statements = getTurtleAsStatements(this.getData());
    }
    if (this.statements) return this.statements;
    return null;
  }
  getOriginalResponseAsString(): string {
    const data = this.getData();
    if (typeof data === "string") {
      return data;
    } else if (this.type === "json") {
      return JSON.stringify(data, undefined, 2);
    }
    return data;
  }

  getType() {
    if (!this.type) this.getAsJson(); //detects type as well
    return this.type;
  }
  getStatus(): number | undefined {
    if (this.res) return this.res.status;
    if (this.summary) return this.summary.status;
  }

  //process the input parameters in such a way that we can store it in local storage (i.e., no function)
  //and, make sure we can easily pass it on back to this wrapper function when loading it again from storage
  //When an object is too large to store, this method returns undefined
  public getAsStoreObject(maxResponseSize: number): Parser.ResponseSummary | undefined {
    let summary = this.summary;
    if (!summary && this.res) {
      summary = {
        contentType: this.getContentType(),
        data: this.getOriginalResponseAsString(),
        error: this.getError(),
        status: this.getStatus(),
        executionTime: this.getResponseTime(),
      };
    }
    if (summary) {
      //if data is set, it should be less than max limit. If not set, it's probably a response error that we'd like to store as well
      if (summary.data && summary.data.length > maxResponseSize) {
        return undefined;
      }
      return summary;
    }
    if (this.error) {
      return {
        error: this.getError(),
        executionTime: this.getResponseTime(),
      };
    }
  }

  public asCsv() {
    const json = this.getAsJson();
    if (this.type === "csv") return this.getOriginalResponseAsString();
    if (json && json.results) {
      return bindingsToCsv(json);
    }
  }
}
export default Parser;
