import type { DatatypesMapperModel } from "@/components/Datatypes/model";
import type { UntypedParameters } from "@/components/Workflow/Editor/modules/parameters";
import type { useWorkflowStores } from "@/composables/workflowStores";
import type { Steps } from "@/stores/workflowStepStore";
import { assertDefined } from "@/utils/assertions";

import { isWorkflowInput } from "../../constants";
import type {
    DisconnectedInputState,
    DuplicateLabelState,
    ExtractInputAction,
    ExtractUntypedParameter,
    LintState,
    MetadataLintState,
    RemoveUnlabeledWorkflowOutputs,
    UnlabeledOuputState,
    UntypedParameterState,
} from "./lintingTypes";
import { terminalFactory } from "./terminals";

export const bestPracticeWarningAnnotation =
    "This workflow does not provide a short description. Providing a short description helps workflow executors understand the purpose and usage of the workflow.";
export const bestPracticeWarningAnnotationLength =
    "This workflow includes a very long short description. The best practice is to break up long descriptions of a workflow into readme and help text and keep the short description field a relatively brief description of the workflow appropriate for displaying in lists of workflows.";
export const bestPracticeWarningCreator =
    "This workflow does not specify creator(s). This is important metadata for workflows that will be published and/or shared to help workflow executors know how to cite the workflow authors.";
export const bestPracticeWarningLicense =
    "This workflow does not specify a license. This is important metadata for workflows that will be published and/or shared to help workflow executors understand how it may be used.";
export const bestPracticeWarningReadme =
    "This workflow does not provide a readme. Providing a detailed readme helps workflow executors understand the details, purpose, and limitations of the workflow.";

export function getDisconnectedInputs(
    steps: Steps = {},
    datatypesMapper: DatatypesMapperModel,
    stores: ReturnType<typeof useWorkflowStores>,
) {
    const inputs: DisconnectedInputState[] = [];
    Object.values(steps).forEach((step) => {
        step.inputs.map((inputSource) => {
            const inputTerminal = terminalFactory(step.id, inputSource, datatypesMapper, stores);
            if (!inputTerminal.optional && inputTerminal.connections.length === 0) {
                const inputLabel = inputSource.label || inputSource.name;
                inputs.push({
                    stepId: step.id,
                    stepLabel: step.label || step.content_id || step.name,
                    warningLabel: inputLabel,
                    inputName: inputSource.name,
                    autofix: !inputTerminal.multiple,
                    highlightType: "input",
                    name: inputSource.name,
                });
            }
        });
    });
    return inputs;
}

export function getMissingMetadata(steps: Steps) {
    const inputs: MetadataLintState[] = [];
    Object.values(steps).forEach((step) => {
        if (isWorkflowInput(step.type)) {
            const noAnnotation = !step.annotation;
            const noLabel = !step.label;
            let warningLabel = null;
            const data: MetadataLintState["data"] = {
                "missing-label": "false",
                "missing-annotation": "false",
            };
            if (noLabel && noAnnotation) {
                warningLabel = "Missing a label and annotation";
                data["missing-label"] = "true";
                data["missing-annotation"] = "true";
            } else if (noLabel) {
                warningLabel = "Missing a label";
                data["missing-label"] = "true";
            } else if (noAnnotation) {
                warningLabel = "Missing an annotation";
                data["missing-annotation"] = "true";
            }
            if (warningLabel) {
                inputs.push({
                    stepId: step.id,
                    stepLabel: step.label || step.content_id || step.name,
                    warningLabel: warningLabel,
                    data: data,
                });
            }
        }
    });
    return inputs;
}

// TODO: Maybe type of action should already be `MetadataLintState`?
export function dataAttributes(action: LintState) {
    const result: Record<string, "true" | "false"> = {};
    // Ensure we have an attributes lint state (`MetadataLintState` with data)
    if (isMetadataLintState(action)) {
        for (const [key, value] of Object.entries(action.data)) {
            result[`data-${key}`] = value;
        }
    }

    return result;
}

export function getDuplicateLabels(steps: Steps, stores: ReturnType<typeof useWorkflowStores>) {
    const duplicates: DuplicateLabelState[] = [];

    const { stepStore } = stores;
    const labels = stepStore.duplicateLabels;

    labels.forEach((label) => {
        Object.values(steps).forEach((step) => {
            const workflowOutputs = step.workflow_outputs || [];
            workflowOutputs.forEach((workflowOutput) => {
                if (workflowOutput.label === label) {
                    duplicates.push({
                        stepId: step.id,
                        stepLabel: step.label || step.content_id || step.name,
                        warningLabel: workflowOutput.output_name,
                        name: workflowOutput.output_name,
                        highlightType: "output",
                    });
                }
            });
        });
    });

    return duplicates;
}

export function getUnlabeledOutputs(steps: Steps) {
    const outputs: UnlabeledOuputState[] = [];
    Object.values(steps).forEach((step) => {
        if (isWorkflowInput(step.type)) {
            // For now skip these... maybe should push this logic into linting though
            // since it is fine to have outputs on inputs.
            return;
        }

        const workflowOutputs = step.workflow_outputs || [];
        for (const workflowOutput of workflowOutputs) {
            if (!workflowOutput.label) {
                outputs.push({
                    stepId: step.id,
                    stepLabel: step.label || step.content_id || step.name,
                    warningLabel: workflowOutput.output_name,
                    autofix: true,
                    highlightType: "output",
                    name: workflowOutput.output_name,
                });
            }
        }
    });
    return outputs;
}

export function getUntypedParameters(untypedParameters: UntypedParameters) {
    const items: UntypedParameterState[] = [];
    if (untypedParameters) {
        untypedParameters.parameters.forEach((parameter) => {
            try {
                const parameterReference = parameter.references[0];
                assertDefined(parameterReference, `Parameter references for ${parameter.name} are empty.`);

                // TODO: Not sure this is right, but I think this may have been broken previously?
                const stepLabel =
                    "toolInput" in parameterReference ? parameterReference.toolInput.label : parameter.name;
                items.push({
                    stepId: parameterReference.stepId,
                    stepLabel: stepLabel,
                    warningLabel: parameter.name,
                    name: parameter.name,
                    autofix: parameter.canExtract(),
                });
            } catch (errorMessage) {
                console.error(errorMessage);
            }
        });
    }
    return items;
}

export function fixAllIssues(
    untypedParameters: UntypedParameterState[],
    disconnectedInputs: DisconnectedInputState[],
    unlabeledOutputs: UnlabeledOuputState[],
) {
    const actions = [];
    for (const untypedParameter of untypedParameters) {
        if (untypedParameter.autofix) {
            actions.push(fixUntypedParameter(untypedParameter));
        }
    }
    for (const disconnectedInput of disconnectedInputs) {
        if (disconnectedInput.autofix) {
            actions.push(fixDisconnectedInput(disconnectedInput));
        }
    }
    if (unlabeledOutputs.length > 0) {
        actions.push(fixUnlabeledOutputs());
    }
    return actions;
}

export function fixUntypedParameter(untypedParameter: UntypedParameterState): ExtractUntypedParameter {
    return {
        action_type: "extract_untyped_parameter",
        name: untypedParameter.name,
    };
}

export function fixDisconnectedInput(disconnectedInput: DisconnectedInputState): ExtractInputAction {
    return {
        action_type: "extract_input",
        input: {
            order_index: disconnectedInput.stepId,
            input_name: disconnectedInput.inputName,
        },
    };
}

export function fixUnlabeledOutputs(): RemoveUnlabeledWorkflowOutputs {
    return { action_type: "remove_unlabeled_workflow_outputs" };
}

export function isDisconnectedInputState(state: LintState): state is DisconnectedInputState {
    return isStateForInputOrOutput(state) && state.highlightType === "input";
}

function isMetadataLintState(state: LintState): state is MetadataLintState {
    return "data" in state;
}

/** Type guard for linting states that are for a workflow step input or output. */
export function isStateForInputOrOutput(
    state: LintState,
): state is DisconnectedInputState | DuplicateLabelState | UnlabeledOuputState {
    return "highlightType" in state;
}
