import { Cache, exists, StateObject } from "@hpcc-js/util";
import { IConnection, IOptions } from "../connection.ts";
import { WsDfu } from "../services/wsDFU.ts";
import { isECLResult, WorkunitsService, WsWorkunits } from "../services/wsWorkunits.ts";
import { parseXSD, XSDSchema, XSDXMLNode } from "./xsdParser.ts";

export class GlobalResultCache extends Cache<{ BaseUrl: string, Wuid: string, ResultName: string }, Result> {
    constructor() {
        super((obj) => {
            return `${obj.BaseUrl}-${obj.Wuid}-${obj.ResultName}`;
        });
    }
}
const _results = new GlobalResultCache();

export type ResultFilter = { [key: string]: string | number };

export interface ECLResultEx extends WsWorkunits.ECLResult {
    Wuid: string;
    ResultName?: string;
    ResultSequence?: number;
    LogicalFileName?: string;
    NodeGroup?: string;
    ResultViews: string[];
}

export interface WUResultResponseEx {

    Exceptions: WsWorkunits.Exceptions;
    Wuid: string;
    Sequence: WsWorkunits.int;
    LogicalName: string;
    Cluster: string;
    Name: string;
    Start: WsWorkunits.long;
    Requested: WsWorkunits.int;
    Count: WsWorkunits.int;
    Total: WsWorkunits.long;
    Result: { [key: string]: any[] } & {
        XmlSchema?: {
            xml: string;
        };
    };
}

export type UResulState = ECLResultEx & WsDfu.DFULogicalFile;
export type IResulState = ECLResultEx | WsDfu.DFULogicalFile;
export class Result extends StateObject<UResulState, IResulState> implements ECLResultEx {
    protected connection: WorkunitsService;
    protected _bypassCache: boolean = false;
    get BaseUrl() { return this.connection.baseUrl; }
    protected xsdSchema: XSDSchema;

    get properties(): WsWorkunits.ECLResult { return this.get(); }
    get Wuid(): string { return this.get("Wuid"); }
    get ResultName(): string | undefined { return this.get("ResultName"); }
    get ResultSequence(): number | undefined { return this.get("ResultSequence"); }
    get LogicalFileName(): string | undefined { return this.get("LogicalFileName"); }
    get Name(): string { return this.get("Name"); }
    get Sequence(): number { return this.get("Sequence"); }
    get Value(): string { return this.get("Value"); }
    get Link(): string { return this.get("Link"); }
    get FileName(): string { return this.get("FileName"); }
    get IsSupplied(): boolean { return this.get("IsSupplied"); }
    get ShowFileContent() { return this.get("ShowFileContent"); }
    get Total(): number { return this.get("Total"); }
    get ECLSchemas(): WsWorkunits.ECLSchemas { return this.get("ECLSchemas"); }
    get NodeGroup(): string { return this.get("NodeGroup"); }
    get ResultViews(): string[] { return this.get("ResultViews"); }
    get XmlSchema(): string { return this.get("XmlSchema"); }

    static attach(optsConnection: IOptions | IConnection | WorkunitsService, wuid: string, name: string);
    static attach(optsConnection: IOptions | IConnection | WorkunitsService, wuid: string, sequence: number);
    static attach(optsConnection: IOptions | IConnection | WorkunitsService, wuid: string, eclResult: WsWorkunits.ECLResult, resultViews: string[]);
    static attach(optsConnection: IOptions | IConnection | WorkunitsService, wuid: string, name_sequence_eclResult?: string | number | WsWorkunits.ECLResult, resultViews?: string[]): Result {
        let retVal: Result;
        if (Array.isArray(resultViews)) {
            retVal = _results.get({ BaseUrl: optsConnection.baseUrl, Wuid: wuid, ResultName: (name_sequence_eclResult as WsWorkunits.ECLResult).Name }, () => {
                return new Result(optsConnection, wuid, name_sequence_eclResult as WsWorkunits.ECLResult, resultViews);
            });
            retVal.set(name_sequence_eclResult as any);
        } else if (typeof resultViews === "undefined") {
            if (typeof name_sequence_eclResult === "number") {
                retVal = _results.get({ BaseUrl: optsConnection.baseUrl, Wuid: wuid, ResultName: "Sequence_" + name_sequence_eclResult }, () => {
                    return new Result(optsConnection, wuid, name_sequence_eclResult);
                });
            } else if (typeof name_sequence_eclResult === "string") {
                retVal = _results.get({ BaseUrl: optsConnection.baseUrl, Wuid: wuid, ResultName: name_sequence_eclResult }, () => {
                    return new Result(optsConnection, wuid, name_sequence_eclResult);
                });
            }
        }
        return retVal;
    }

    static attachLogicalFile(optsConnection: IOptions | IConnection | WorkunitsService, nodeGroup: string, logicalFile: string) {
        return _results.get({ BaseUrl: optsConnection.baseUrl, Wuid: nodeGroup, ResultName: logicalFile }, () => {
            return new Result(optsConnection, nodeGroup, logicalFile, true);
        });
    }

    private constructor(optsConnection: IOptions | IConnection | WorkunitsService, wuid: string, name: string);
    private constructor(optsConnection: IOptions | IConnection | WorkunitsService, wuid: string, sequence: number);
    private constructor(optsConnection: IOptions | IConnection | WorkunitsService, wuid: string, eclResult: WsWorkunits.ECLResult, resultViews: string[]);
    private constructor(optsConnection: IOptions | IConnection | WorkunitsService, nodeGroup: string, logicalFile: string, isLogicalFiles: boolean);
    private constructor(optsConnection: IOptions | IConnection | WorkunitsService, wuid_NodeGroup: string, name_sequence_eclResult_logicalFile?: string | number | WsWorkunits.ECLResult, resultViews_isLogicalFile?: any[] | boolean) {
        super();
        if (optsConnection instanceof WorkunitsService) {
            this.connection = optsConnection;
        } else {
            this.connection = new WorkunitsService(optsConnection);
        }

        if (typeof resultViews_isLogicalFile === "boolean" && resultViews_isLogicalFile === true) {
            this.set({
                NodeGroup: wuid_NodeGroup,
                LogicalFileName: name_sequence_eclResult_logicalFile
            } as ECLResultEx);
        } else if (isECLResult(name_sequence_eclResult_logicalFile) && Array.isArray(resultViews_isLogicalFile)) {
            this.set({
                ...name_sequence_eclResult_logicalFile,
                Wuid: wuid_NodeGroup,
                ResultName: name_sequence_eclResult_logicalFile.Name,
                ResultViews: resultViews_isLogicalFile
            } as ECLResultEx);
        } else if (typeof resultViews_isLogicalFile === "undefined") {
            if (typeof name_sequence_eclResult_logicalFile === "number") {
                this.set({
                    Wuid: wuid_NodeGroup,
                    ResultSequence: name_sequence_eclResult_logicalFile
                } as ECLResultEx);
            } else if (typeof name_sequence_eclResult_logicalFile === "string") {
                this.set({
                    Wuid: wuid_NodeGroup,
                    ResultName: name_sequence_eclResult_logicalFile
                } as ECLResultEx);
            } else {
                console.warn("Unknown Result.attach (1)");
            }
        } else {
            console.warn("Unknown Result.attach (2)");
        }
    }

    isComplete() {
        return this.Total !== -1;
    }

    bypassCache(bypass?: boolean): boolean | this {
        if (bypass === undefined) return this._bypassCache;
        this._bypassCache = bypass;
        return this;
    }

    private _fetchXMLSchemaPromise: Promise<XSDSchema | null>;
    fetchXMLSchema(refresh = false): Promise<XSDSchema | null> {
        if (!this._fetchXMLSchemaPromise || refresh) {
            this._fetchXMLSchemaPromise = this.WUResult().then(response => {
                if (response.Result?.XmlSchema?.xml) {
                    this.xsdSchema = parseXSD(response.Result.XmlSchema.xml);
                    return this.xsdSchema;
                }
                return null;
            });
        }
        return this._fetchXMLSchemaPromise;
    }

    async refresh(): Promise<this> {
        await this.fetchRows(0, 1, true);
        return this;
    }

    fetchRows(from: number = 0, count: number = -1, includeSchema: boolean = false, filter: ResultFilter = {}, abortSignal?: AbortSignal, bypassCache?: boolean): Promise<any[]> {
        const shouldBypassCache = bypassCache ?? this._bypassCache;
        return this.WUResult(from, count, !includeSchema, filter, abortSignal, shouldBypassCache).then((response) => {
            const result: any = response.Result;
            delete response.Result; //  Do not want it in "set"
            this.set({
                ...response
            } as any);
            if (exists("XmlSchema.xml", result)) {
                this.xsdSchema = parseXSD(result.XmlSchema.xml);
            }
            if (exists("Row", result)) {
                return result.Row;
            } else if (this.ResultName && exists(this.ResultName, result)) {
                return result[this.ResultName].Row;
            }
            return [];
        });
    }

    rootField(): XSDXMLNode | null {
        if (!this.xsdSchema) return null;
        return this.xsdSchema.root;
    }

    fields(): XSDXMLNode[] {
        if (!this.xsdSchema) return [];
        return this.xsdSchema.root.children();
    }

    protected WUResult(start: number = 0, count: number = 1, suppressXmlSchema: boolean = false, filter: { [key: string]: string | number } = {}, abortSignal?: AbortSignal, bypassCache: boolean = false): Promise<WUResultResponseEx> {
        const FilterBy = {
            NamedValue: {
                itemcount: 0
            }
        };
        for (const key in filter) {
            FilterBy.NamedValue[FilterBy.NamedValue.itemcount++] = {
                Name: key,
                Value: filter[key]
            };
        }
        const request: Partial<WsWorkunits.WUResult> = { FilterBy } as any;
        if (this.Wuid && this.ResultName !== undefined) {
            request.Wuid = this.Wuid;
            request.ResultName = this.ResultName;
        } else if (this.Wuid && this.ResultSequence !== undefined) {
            request.Wuid = this.Wuid;
            request.Sequence = this.ResultSequence;
        } else if (this.LogicalFileName && this.NodeGroup) {
            request.LogicalName = this.LogicalFileName;
            request.Cluster = this.NodeGroup;
        } else if (this.LogicalFileName) {
            request.LogicalName = this.LogicalFileName;
        }
        request.Start = start;
        request.Count = count;
        request.SuppressXmlSchema = suppressXmlSchema;
        request.BypassCachedResult = bypassCache;
        return this.connection.WUResult(request, abortSignal).then((response: unknown) => {
            return response as WUResultResponseEx;
        });
    }
}

export class ResultCache extends Cache<WsWorkunits.ECLResult, Result> {
    constructor() {
        super((obj) => {
            return Cache.hash([obj.Sequence, obj.Name, obj.Value, obj.FileName]);
        });
    }
}
