# -*- coding: utf-8 -*-
from __future__ import annotations

import os
import json

from qgis.PyQt.QtCore import QCoreApplication, QDateTime
from qgis.PyQt.QtGui import QIcon
from qgis.core import (
    QgsProcessingAlgorithm,
    QgsProcessingException,
    QgsProcessingParameterString,
    QgsProcessingParameterEnum,
    QgsProcessingParameterFileDestination,
    QgsProcessingParameterDateTime,
    QgsProcessingParameterBoolean,
    QgsProcessingParameterRasterLayer,
    QgsProject,
)
from .arraystorage_core import (
    ArrayStorageRasterRequest,
    load_arraystorage_raster,
    list_raster_timeseries,
    list_timestamps,
    make_session,
    qdt_to_iso_with_tz,
    delete_metadata,
    delete_data,
    build_timeseries_layer,
)
from .arraystore_classifications import (
    raster_style_to_classification,
    upload_classification,
    list_classifications,
    build_classifications_layer,
    delete_classification,
    create_dummy_raster_layer,
    apply_classification_to_layer,
)


class _BaseArrayStorageAlgorithm(QgsProcessingAlgorithm):
    """
    Shared helpers for ArrayStorage algorithms.
    """

    PARAM_BASE_URL = "BASE_URL"
    PARAM_AS_USER = "AS_USER"
    PARAM_AS_PASS = "AS_PASS"
    PARAM_STORE_ID = "STORE_ID"

    def createInstance(self):
        return self.__class__()

    def tr(self, string: str) -> str:
        return QCoreApplication.translate("Processing", string)

    def groupId(self) -> str:
        return "ArrayStorage"

    def group(self) -> str:
        return self.tr("ArrayStorage")

    def icon(self):
        plugin_path = os.path.dirname(__file__)
        return QIcon(os.path.join(plugin_path, "icon_arraystorage.png"))

    def base_url_param(self):
        return QgsProcessingParameterString(
            self.PARAM_BASE_URL,
            self.tr("Base URL"),
            defaultValue="http://localhost:8013",
        )

    def as_user_param(self):
        return QgsProcessingParameterString(
            self.PARAM_AS_USER,
            self.tr("Username"),
            optional=True,
        )

    def as_pass_param(self):
        return QgsProcessingParameterString(
            self.PARAM_AS_PASS,
            self.tr("Password"),
            optional=True,
        )

    def store_id_param(self):
        return QgsProcessingParameterString(
            self.PARAM_STORE_ID,
            self.tr("Store ID (optional)"),
            optional=True,
        )

    def flags(self):
        # Keep default threading behaviour (you can force main-thread if needed)
        return super().flags()


# ---------------------------------------------------------------------------
# data: load a GeoTIFF layer from ArrayStorage
# ---------------------------------------------------------------------------


class ArrayStorageLoadRasterAlgorithm(_BaseArrayStorageAlgorithm):
    """
    Downloads a raster (GeoTIFF) from ArrayStorage and loads it into the project.
    """

    PARAM_IDENT_TYPE = "IDENT_TYPE"
    PARAM_IDENTIFIER = "IDENTIFIER"
    PARAM_TIME = "RASTER_TIME"
    PARAM_TARGET_FILE = "TARGET_FILE"
    PARAM_T0 = "T0_TIME"

    IDENT_TYPE_OPTIONS = ["ts_id", "path"]

    def name(self) -> str:
        return "arraystorage_load_raster"

    def displayName(self) -> str:
        return self.tr("Load Raster from ArrayStorage")

    def shortHelpString(self) -> str:
        return self.tr(
            "Downloads a raster product (GeoTIFF) from ArrayStorage by ts_id or path "
            "and loads it into the current project. All ArrayStorage metadata is "
            "attached as custom layer properties."
        )

    def icon(self):
        plugin_path = os.path.dirname(__file__)
        return QIcon(os.path.join(plugin_path, "icon_arraystore_download.png"))

    def ident_type_param(self):
        return QgsProcessingParameterEnum(
            self.PARAM_IDENT_TYPE,
            self.tr("Identifier type"),
            options=self.IDENT_TYPE_OPTIONS,
            defaultValue=1,  # path
        )

    def identifier_param(self):
        return QgsProcessingParameterString(
            self.PARAM_IDENTIFIER,
            self.tr("Identifier (ts_id or path)"),
        )

    def t0_param(self):
        # Optional t0; if left empty, we only select by time
        return QgsProcessingParameterDateTime(
            self.PARAM_T0,
            self.tr("t0 (optional, UTC)"),
            type=QgsProcessingParameterDateTime.DateTime,
            optional=True,
        )

    def time_param(self):
        return QgsProcessingParameterDateTime(
            self.PARAM_TIME,
            self.tr("Raster time"),
            type=QgsProcessingParameterDateTime.DateTime,
        )

    def target_file_param(self):
        return QgsProcessingParameterFileDestination(
            self.PARAM_TARGET_FILE,
            self.tr("Output GeoTIFF"),
            fileFilter="GeoTIFF (*.tif *.tiff)",
        )

    def initAlgorithm(self, config):
        # You can pre-populate creds & base_url from QSettings, similar to Datasphere;
        # for now we just expose simple fields.
        self.addParameter(self.base_url_param())
        self.addParameter(self.as_user_param())
        self.addParameter(self.as_pass_param())
        self.addParameter(self.store_id_param())
        self.addParameter(self.ident_type_param())
        self.addParameter(self.identifier_param())
        self.addParameter(self.time_param())
        self.addParameter(self.t0_param())
        self.addParameter(self.target_file_param())

    def processAlgorithm(self, parameters, context, feedback):
        base_url = self.parameterAsString(parameters, self.PARAM_BASE_URL, context)
        as_user = (
            self.parameterAsString(parameters, self.PARAM_AS_USER, context) or None
        )
        as_pass = (
            self.parameterAsString(parameters, self.PARAM_AS_PASS, context) or None
        )
        store_id = (
            self.parameterAsString(parameters, self.PARAM_STORE_ID, context) or None
        )

        ident_type_idx = self.parameterAsEnum(
            parameters, self.PARAM_IDENT_TYPE, context
        )
        ident_type = self.IDENT_TYPE_OPTIONS[ident_type_idx]

        identifier = self.parameterAsString(
            parameters, self.PARAM_IDENTIFIER, context
        ).strip()
        if not identifier:
            raise QgsProcessingException("Identifier (ts_id or path) is required.")

        time_dt: QDateTime = self.parameterAsDateTime(
            parameters, self.PARAM_TIME, context
        )
        if not time_dt.isValid():
            raise QgsProcessingException("Raster timestamp is required.")

        # Optional t0
        t0_dt: QDateTime = self.parameterAsDateTime(parameters, self.PARAM_T0, context)
        if not t0_dt.isValid():
            t0_dt = None

        target_file = self.parameterAsFileOutput(
            parameters, self.PARAM_TARGET_FILE, context
        )

        req = ArrayStorageRasterRequest(
            base_url=base_url,
            ident_type=ident_type,
            identifier=identifier,
            time_dt=time_dt,
            t0_dt=t0_dt,
            as_user=as_user,
            as_pass=as_pass,
            target_file=target_file,
            store_id=store_id,
        )

        result = load_arraystorage_raster(
            req,
            feedback=feedback,
            add_to_project=True,
        )

        return {self.PARAM_TARGET_FILE: result.tif_path}


# ---------------------------------------------------------------------------
# list_timeseries: list rasterTimeSeries JSON
# ---------------------------------------------------------------------------


class ArrayStorageListTimeseriesAlgorithm(_BaseArrayStorageAlgorithm):
    """
    Lists ArrayStorage raster time series, writes them as JSON,
    and adds a layer (with optional geometry) to the project.
    """

    PARAM_OUTPUT = "OUTPUT_JSON"
    PARAM_ADD_GEOM = "ADD_GEOMETRY"

    def name(self) -> str:
        return "arraystorage_list_timeseries"

    def displayName(self) -> str:
        return self.tr("List Raster Time Series (ArrayStorage)")

    def shortHelpString(self) -> str:
        return self.tr(
            "Lists all raster time series metadata from ArrayStorage, writes the "
            "result to a JSON file, and adds a no-geometry table layer with "
            "normalized fields to the project."
        )

    def icon(self):
        # Use the table icon you specified
        import os

        plugin_path = os.path.dirname(__file__)
        return QIcon(os.path.join(plugin_path, "icon_arraystore_table_dock.png"))

    def output_param(self):
        return QgsProcessingParameterFileDestination(
            self.PARAM_OUTPUT,
            self.tr("Output JSON"),
            fileFilter="JSON (*.json)",
        )

    def add_geom_param(self):
        return QgsProcessingParameterBoolean(
            self.PARAM_ADD_GEOM,
            self.tr("Add geometry from geometryWkt/boundingBox"),
            defaultValue=True,
        )

    def initAlgorithm(self, config):
        self.addParameter(self.base_url_param())
        self.addParameter(self.as_user_param())
        self.addParameter(self.as_pass_param())
        self.addParameter(self.store_id_param())
        self.addParameter(self.output_param())
        self.addParameter(self.add_geom_param())

    def processAlgorithm(self, parameters, context, feedback):
        base_url = self.parameterAsString(parameters, self.PARAM_BASE_URL, context)
        as_user = (
            self.parameterAsString(parameters, self.PARAM_AS_USER, context) or None
        )
        as_pass = (
            self.parameterAsString(parameters, self.PARAM_AS_PASS, context) or None
        )
        store_id = (
            self.parameterAsString(parameters, self.PARAM_STORE_ID, context) or None
        )

        add_geom = self.parameterAsBool(parameters, self.PARAM_ADD_GEOM, context)
        output_path = self.parameterAsFileOutput(parameters, self.PARAM_OUTPUT, context)

        session = make_session(as_user, as_pass)
        items = list_raster_timeseries(base_url, session, store_id=store_id)

        # 1) write JSON to disk (as before)
        os.makedirs(os.path.dirname(output_path), exist_ok=True)
        with open(output_path, "w", encoding="utf-8") as f:
            json.dump(items, f, indent=2, ensure_ascii=False)

        feedback.pushInfo(f"Wrote {len(items)} raster time series to {output_path}")

        # 2) create and add a layer to the project
        layer = build_timeseries_layer(items, add_geometry=add_geom)
        if not layer.isValid():
            raise QgsProcessingException(
                "Failed to create in-memory layer for raster time series metadata."
            )

        QgsProject.instance().addMapLayer(layer)
        feedback.pushInfo(
            f"Added table layer with {layer.featureCount()} rows "
            f"for ArrayStorage raster time series."
        )

        return {
            self.PARAM_OUTPUT: output_path,
        }


# ---------------------------------------------------------------------------
# timestamps: list timeStamps for a given ts_id
# ---------------------------------------------------------------------------


class ArrayStorageListTimestampsAlgorithm(_BaseArrayStorageAlgorithm):
    """
    Lists available timestamps for a given raster time series.
    """

    PARAM_TS_ID = "TS_ID"
    PARAM_FROM = "FROM_TIME"
    PARAM_UNTIL = "UNTIL_TIME"
    PARAM_FACTOR = "FACTOR"
    PARAM_OUTPUT = "OUTPUT_JSON"

    def name(self) -> str:
        return "arraystorage_list_timestamps"

    def displayName(self) -> str:
        return self.tr("List Timestamps (ArrayStorage)")

    def shortHelpString(self) -> str:
        return self.tr(
            "Lists available timestamps for a given ArrayStorage raster time "
            "series and writes them as JSON."
        )

    def icon(self):
        plugin_path = os.path.dirname(__file__)
        return QIcon(os.path.join(plugin_path, "icon_arraystore_table_dock.png"))

    def ts_id_param(self):
        return QgsProcessingParameterString(
            self.PARAM_TS_ID,
            self.tr("Timeseries ID (ts_id)"),
        )

    def from_param(self):
        return QgsProcessingParameterDateTime(
            self.PARAM_FROM,
            self.tr("From (optional)"),
            type=QgsProcessingParameterDateTime.DateTime,
            optional=True,
        )

    def until_param(self):
        return QgsProcessingParameterDateTime(
            self.PARAM_UNTIL,
            self.tr("Until (optional)"),
            type=QgsProcessingParameterDateTime.DateTime,
            optional=True,
        )

    def factor_param(self):
        return QgsProcessingParameterString(
            self.PARAM_FACTOR,
            self.tr("Factor (subsampling, optional)"),
            optional=True,
        )

    def output_param(self):
        return QgsProcessingParameterFileDestination(
            self.PARAM_OUTPUT,
            self.tr("Output JSON"),
            fileFilter="JSON (*.json)",
        )

    def initAlgorithm(self, config):
        self.addParameter(self.base_url_param())
        self.addParameter(self.as_user_param())
        self.addParameter(self.as_pass_param())
        self.addParameter(self.store_id_param())
        self.addParameter(self.ts_id_param())
        self.addParameter(self.from_param())
        self.addParameter(self.until_param())
        self.addParameter(self.factor_param())
        self.addParameter(self.output_param())

    def processAlgorithm(self, parameters, context, feedback):
        base_url = self.parameterAsString(parameters, self.PARAM_BASE_URL, context)
        as_user = (
            self.parameterAsString(parameters, self.PARAM_AS_USER, context) or None
        )
        as_pass = (
            self.parameterAsString(parameters, self.PARAM_AS_PASS, context) or None
        )
        store_id = (
            self.parameterAsString(parameters, self.PARAM_STORE_ID, context) or None
        )

        ts_id = self.parameterAsString(parameters, self.PARAM_TS_ID, context).strip()
        if not ts_id:
            raise QgsProcessingException("TS_ID is required.")

        from_dt: QDateTime = self.parameterAsDateTime(
            parameters, self.PARAM_FROM, context
        )
        until_dt: QDateTime = self.parameterAsDateTime(
            parameters, self.PARAM_UNTIL, context
        )

        from_iso = qdt_to_iso_with_tz(from_dt) if from_dt.isValid() else None
        until_iso = qdt_to_iso_with_tz(until_dt) if until_dt.isValid() else None

        factor_str = self.parameterAsString(
            parameters, self.PARAM_FACTOR, context
        ).strip()
        factor = int(factor_str) if factor_str else None

        output_path = self.parameterAsFileOutput(parameters, self.PARAM_OUTPUT, context)

        session = make_session(as_user, as_pass)
        data = list_timestamps(
            base_url,
            ts_id,
            session,
            store_id=store_id,
            from_iso=from_iso,
            until_iso=until_iso,
            factor=factor,
        )

        os.makedirs(os.path.dirname(output_path), exist_ok=True)
        with open(output_path, "w", encoding="utf-8") as f:
            json.dump(data, f, indent=2, ensure_ascii=False)

        feedback.pushInfo(
            f"Wrote {len(data)} timestamps entries for ts_id={ts_id} to {output_path}"
        )
        return {self.PARAM_OUTPUT: output_path}


# ---------------------------------------------------------------------------
# delete_meta: delete tensor/raster metadata
# ---------------------------------------------------------------------------


class ArrayStorageDeleteMetaAlgorithm(_BaseArrayStorageAlgorithm):
    """
    Deletes tensor/raster metadata from ArrayStorage using ts_paths and/or group_paths.
    """

    PARAM_TS_PATHS = "TS_PATHS"
    PARAM_GROUP_PATHS = "GROUP_PATHS"
    PARAM_DELETE_GROUPS = "DELETE_GROUPS"
    PARAM_DELETE_PARENT = "DELETE_PARENT_METADATA"
    PARAM_MODE = "MODE"
    PARAM_OUTPUT = "OUTPUT_JSON"

    def name(self) -> str:
        return "arraystorage_delete_meta"

    def displayName(self) -> str:
        return self.tr("Delete Metadata (ArrayStorage)")

    def shortHelpString(self) -> str:
        return self.tr(
            "Calls DELETE /rest/arrayStorage/tensorTimeSeries to delete metadata "
            "for the given ts_paths and/or group_paths."
        )

    def icon(self):
        plugin_path = os.path.dirname(__file__)
        return QIcon(os.path.join(plugin_path, "icon_arraystore_delete.png"))

    def ts_paths_param(self):
        return QgsProcessingParameterString(
            self.PARAM_TS_PATHS,
            self.tr("Timeseries paths (comma or newline separated)"),
            optional=True,
            multiLine=True,
        )

    def group_paths_param(self):
        return QgsProcessingParameterString(
            self.PARAM_GROUP_PATHS,
            self.tr("Group paths (comma or newline separated)"),
            optional=True,
            multiLine=True,
        )

    def delete_groups_param(self):
        return QgsProcessingParameterEnum(
            self.PARAM_DELETE_GROUPS,
            self.tr("Delete groups"),
            options=["False", "True"],
            defaultValue=0,
        )

    def delete_parent_param(self):
        return QgsProcessingParameterEnum(
            self.PARAM_DELETE_PARENT,
            self.tr("Delete parent metadata"),
            options=["False", "True"],
            defaultValue=0,
        )

    def mode_param(self):
        return QgsProcessingParameterString(
            self.PARAM_MODE,
            self.tr("Mode (strict|soft, etc.)"),
            defaultValue="strict",
            optional=True,
        )

    def output_param(self):
        return QgsProcessingParameterFileDestination(
            self.PARAM_OUTPUT,
            self.tr("Output JSON (response)"),
            fileFilter="JSON (*.json)",
        )

    def initAlgorithm(self, config):
        self.addParameter(self.base_url_param())
        self.addParameter(self.as_user_param())
        self.addParameter(self.as_pass_param())
        self.addParameter(self.store_id_param())
        self.addParameter(self.ts_paths_param())
        self.addParameter(self.group_paths_param())
        self.addParameter(self.delete_groups_param())
        self.addParameter(self.delete_parent_param())
        self.addParameter(self.mode_param())
        self.addParameter(self.output_param())

    def _split_multi(self, raw: str) -> list[str]:
        items: list[str] = []
        for part in raw.replace(",", "\n").splitlines():
            p = part.strip()
            if p:
                items.append(p)
        return items

    def processAlgorithm(self, parameters, context, feedback):
        base_url = self.parameterAsString(parameters, self.PARAM_BASE_URL, context)
        as_user = (
            self.parameterAsString(parameters, self.PARAM_AS_USER, context) or None
        )
        as_pass = (
            self.parameterAsString(parameters, self.PARAM_AS_PASS, context) or None
        )
        store_id = (
            self.parameterAsString(parameters, self.PARAM_STORE_ID, context) or None
        )

        raw_ts_paths = self.parameterAsString(parameters, self.PARAM_TS_PATHS, context)
        raw_group_paths = self.parameterAsString(
            parameters, self.PARAM_GROUP_PATHS, context
        )

        ts_paths = self._split_multi(raw_ts_paths) if raw_ts_paths else None
        group_paths = self._split_multi(raw_group_paths) if raw_group_paths else None

        del_groups_idx = self.parameterAsEnum(
            parameters, self.PARAM_DELETE_GROUPS, context
        )
        del_parent_idx = self.parameterAsEnum(
            parameters, self.PARAM_DELETE_PARENT, context
        )

        delete_groups = bool(del_groups_idx)
        delete_parent = bool(del_parent_idx)

        mode = (
            self.parameterAsString(parameters, self.PARAM_MODE, context).strip()
            or "strict"
        )

        output_path = self.parameterAsFileOutput(parameters, self.PARAM_OUTPUT, context)

        session = make_session(as_user, as_pass)
        resp = delete_metadata(
            base_url,
            ts_paths=ts_paths,
            group_paths=group_paths,
            delete_groups=delete_groups,
            delete_parent_metadata=delete_parent,
            mode=mode,
            store_id=store_id,
            session=session,
        )

        os.makedirs(os.path.dirname(output_path), exist_ok=True)
        with open(output_path, "w", encoding="utf-8") as f:
            json.dump(resp, f, indent=2, ensure_ascii=False)

        feedback.pushInfo(
            f"ArrayStorage delete_metadata response saved to {output_path}"
        )
        return {self.PARAM_OUTPUT: output_path}


# ---------------------------------------------------------------------------
# delete_data: delete data slices
# ---------------------------------------------------------------------------


class ArrayStorageDeleteDataAlgorithm(_BaseArrayStorageAlgorithm):
    """
    Deletes data slices from ArrayStorage for a given ts_path.
    """

    PARAM_TS_PATH = "TS_PATH"
    PARAM_FROM = "FROM_TIME"
    PARAM_UNTIL = "UNTIL_TIME"
    PARAM_T0 = "T0_TIME"
    PARAM_MEMBER = "MEMBER"
    PARAM_DISPATCH_INFO = "DISPATCH_INFO"
    PARAM_OUTPUT = "OUTPUT_JSON"

    def name(self) -> str:
        return "arraystorage_delete_data"

    def displayName(self) -> str:
        return self.tr("Delete Data (ArrayStorage)")

    def shortHelpString(self) -> str:
        return self.tr(
            "Calls DELETE /rest/arrayStorage/tensorTimeSeries/data/{ts_path} to "
            "delete data slices, optionally restricted by from/until/t0/member/dispatch_info."
        )

    def icon(self):
        plugin_path = os.path.dirname(__file__)
        return QIcon(os.path.join(plugin_path, "icon_arraystore_delete.png"))

    def ts_path_param(self):
        return QgsProcessingParameterString(
            self.PARAM_TS_PATH,
            self.tr("Timeseries path"),
        )

    def from_param(self):
        return QgsProcessingParameterDateTime(
            self.PARAM_FROM,
            self.tr("From (optional)"),
            type=QgsProcessingParameterDateTime.DateTime,
            optional=True,
        )

    def until_param(self):
        return QgsProcessingParameterDateTime(
            self.PARAM_UNTIL,
            self.tr("Until (optional)"),
            type=QgsProcessingParameterDateTime.DateTime,
            optional=True,
        )

    def t0_param(self):
        return QgsProcessingParameterDateTime(
            self.PARAM_T0,
            self.tr("t0 (optional)"),
            type=QgsProcessingParameterDateTime.DateTime,
            optional=True,
        )

    def member_param(self):
        return QgsProcessingParameterString(
            self.PARAM_MEMBER,
            self.tr("Member (optional)"),
            optional=True,
        )

    def dispatch_info_param(self):
        return QgsProcessingParameterString(
            self.PARAM_DISPATCH_INFO,
            self.tr("Dispatch info (optional)"),
            optional=True,
        )

    def output_param(self):
        return QgsProcessingParameterFileDestination(
            self.PARAM_OUTPUT,
            self.tr("Output JSON (response)"),
            fileFilter="JSON (*.json)",
        )

    def initAlgorithm(self, config):
        self.addParameter(self.base_url_param())
        self.addParameter(self.as_user_param())
        self.addParameter(self.as_pass_param())
        self.addParameter(self.store_id_param())
        self.addParameter(self.ts_path_param())
        self.addParameter(self.from_param())
        self.addParameter(self.until_param())
        self.addParameter(self.t0_param())
        self.addParameter(self.member_param())
        self.addParameter(self.dispatch_info_param())
        self.addParameter(self.output_param())

    def processAlgorithm(self, parameters, context, feedback):
        base_url = self.parameterAsString(parameters, self.PARAM_BASE_URL, context)
        as_user = (
            self.parameterAsString(parameters, self.PARAM_AS_USER, context) or None
        )
        as_pass = (
            self.parameterAsString(parameters, self.PARAM_AS_PASS, context) or None
        )
        store_id = (
            self.parameterAsString(parameters, self.PARAM_STORE_ID, context) or None
        )

        ts_path = self.parameterAsString(
            parameters, self.PARAM_TS_PATH, context
        ).strip()
        if not ts_path:
            raise QgsProcessingException("TS_PATH is required.")

        from_dt: QDateTime = self.parameterAsDateTime(
            parameters, self.PARAM_FROM, context
        )
        until_dt: QDateTime = self.parameterAsDateTime(
            parameters, self.PARAM_UNTIL, context
        )
        t0_dt: QDateTime = self.parameterAsDateTime(parameters, self.PARAM_T0, context)

        from_iso = qdt_to_iso_with_tz(from_dt) if from_dt.isValid() else None
        until_iso = qdt_to_iso_with_tz(until_dt) if until_dt.isValid() else None
        t0_iso = qdt_to_iso_with_tz(t0_dt) if t0_dt.isValid() else None

        member = (
            self.parameterAsString(parameters, self.PARAM_MEMBER, context).strip()
            or None
        )
        dispatch_info = (
            self.parameterAsString(
                parameters, self.PARAM_DISPATCH_INFO, context
            ).strip()
            or None
        )

        output_path = self.parameterAsFileOutput(parameters, self.PARAM_OUTPUT, context)

        session = make_session(as_user, as_pass)
        resp = delete_data(
            base_url,
            ts_path,
            store_id=store_id,
            from_iso=from_iso,
            until_iso=until_iso,
            t0_iso=t0_iso,
            member=member,
            dispatch_info=dispatch_info,
            session=session,
        )

        os.makedirs(os.path.dirname(output_path), exist_ok=True)
        with open(output_path, "w", encoding="utf-8") as f:
            json.dump(resp, f, indent=2, ensure_ascii=False)

        feedback.pushInfo(f"ArrayStorage delete_data response saved to {output_path}")
        return {self.PARAM_OUTPUT: output_path}


# ---------------------------------------------------------------------------
# classifications: upload/list/delete
# ---------------------------------------------------------------------------


class ArrayStorageUploadClassificationAlgorithm(_BaseArrayStorageAlgorithm):
    """
    Uploads the current raster style (pseudocolor) as an ArrayStorage classification.
    """

    PARAM_LAYER = "RASTER_LAYER"
    PARAM_CLASSIFICATION_NAME = "CLASSIFICATION_NAME"
    PARAM_PARAMETER_NAME = "PARAMETER_NAME"
    PARAM_DESCRIPTION = "DESCRIPTION"
    PARAM_OUTPUT = "OUTPUT_JSON"

    def name(self) -> str:
        return "arraystorage_upload_classification"

    def displayName(self) -> str:
        return self.tr("Upload Raster Style as Classification")

    def shortHelpString(self) -> str:
        return self.tr(
            "Builds an ArrayStorage classification from the selected raster layer's "
            "pseudocolor renderer and uploads it via PUT /rest/arrayStorage/classifications."
        )

    def icon(self):
        plugin_path = os.path.dirname(__file__)
        return QIcon(os.path.join(plugin_path, "icon_arraystore_create.png"))

    def layer_param(self):
        return QgsProcessingParameterRasterLayer(
            self.PARAM_LAYER,
            self.tr("Raster layer (pseudocolor)"),
        )

    def classification_name_param(self):
        return QgsProcessingParameterString(
            self.PARAM_CLASSIFICATION_NAME,
            self.tr("Classification name"),
        )

    def parameter_name_param(self):
        return QgsProcessingParameterString(
            self.PARAM_PARAMETER_NAME,
            self.tr("Parameter name (* for all)"),
            defaultValue="*",
            optional=True,
        )

    def description_param(self):
        return QgsProcessingParameterString(
            self.PARAM_DESCRIPTION,
            self.tr("Description (optional)"),
            optional=True,
        )

    def output_param(self):
        return QgsProcessingParameterFileDestination(
            self.PARAM_OUTPUT,
            self.tr("Output JSON (response)"),
            fileFilter="JSON (*.json)",
            optional=True,
        )

    def initAlgorithm(self, config):
        self.addParameter(self.base_url_param())
        self.addParameter(self.as_user_param())
        self.addParameter(self.as_pass_param())
        self.addParameter(self.layer_param())
        self.addParameter(self.classification_name_param())
        self.addParameter(self.parameter_name_param())
        self.addParameter(self.description_param())
        self.addParameter(self.output_param())

    def processAlgorithm(self, parameters, context, feedback):
        base_url = self.parameterAsString(parameters, self.PARAM_BASE_URL, context)
        as_user = (
            self.parameterAsString(parameters, self.PARAM_AS_USER, context) or None
        )
        as_pass = (
            self.parameterAsString(parameters, self.PARAM_AS_PASS, context) or None
        )

        layer = self.parameterAsRasterLayer(parameters, self.PARAM_LAYER, context)
        if layer is None or not layer.isValid():
            raise QgsProcessingException("Please choose a valid raster layer")

        classification_name = self.parameterAsString(
            parameters, self.PARAM_CLASSIFICATION_NAME, context
        ).strip()
        if not classification_name:
            raise QgsProcessingException("Classification name is required")

        parameter_name = (
            self.parameterAsString(parameters, self.PARAM_PARAMETER_NAME, context)
            or "*"
        ).strip()
        description = (
            self.parameterAsString(parameters, self.PARAM_DESCRIPTION, context).strip()
            or None
        )

        output_path = self.parameterAsString(parameters, self.PARAM_OUTPUT, context)
        if output_path:
            output_path = self.parameterAsFileOutput(
                parameters, self.PARAM_OUTPUT, context
            )
            os.makedirs(os.path.dirname(output_path), exist_ok=True)

        classification = raster_style_to_classification(
            layer,
            classification_name,
            parameter_name=parameter_name or "*",
            description=description,
        )

        session = make_session(as_user, as_pass)
        resp = upload_classification(base_url, classification, session=session)

        if output_path:
            with open(output_path, "w", encoding="utf-8") as f:
                json.dump(resp, f, indent=2, ensure_ascii=False)
            feedback.pushInfo(f"Saved classification response to {output_path}")

        return {self.PARAM_OUTPUT: output_path or ""}


class ArrayStorageListClassificationsAlgorithm(_BaseArrayStorageAlgorithm):
    """
    Lists classifications and loads them as a no-geometry table layer.
    """

    PARAM_OUTPUT = "OUTPUT_JSON"
    PARAM_ADD_LAYER = "ADD_LAYER"

    def name(self) -> str:
        return "arraystorage_list_classifications"

    def displayName(self) -> str:
        return self.tr("List Classifications")

    def shortHelpString(self) -> str:
        return self.tr(
            "Calls GET /rest/arrayStorage/classifications, writes the JSON response "
            "to disk, and adds a normalized no-geometry layer to the project."
        )

    def icon(self):
        plugin_path = os.path.dirname(__file__)
        return QIcon(os.path.join(plugin_path, "icon_arraystore_table_dock.png"))

    def output_param(self):
        return QgsProcessingParameterFileDestination(
            self.PARAM_OUTPUT,
            self.tr("Output JSON"),
            fileFilter="JSON (*.json)",
        )

    def add_layer_param(self):
        return QgsProcessingParameterBoolean(
            self.PARAM_ADD_LAYER,
            self.tr("Add layer to project"),
            defaultValue=True,
        )

    def initAlgorithm(self, config):
        self.addParameter(self.base_url_param())
        self.addParameter(self.as_user_param())
        self.addParameter(self.as_pass_param())
        self.addParameter(self.output_param())
        self.addParameter(self.add_layer_param())

    def processAlgorithm(self, parameters, context, feedback):
        base_url = self.parameterAsString(parameters, self.PARAM_BASE_URL, context)
        as_user = (
            self.parameterAsString(parameters, self.PARAM_AS_USER, context) or None
        )
        as_pass = (
            self.parameterAsString(parameters, self.PARAM_AS_PASS, context) or None
        )
        output_path = self.parameterAsFileOutput(parameters, self.PARAM_OUTPUT, context)
        add_layer = self.parameterAsBool(parameters, self.PARAM_ADD_LAYER, context)

        session = make_session(as_user, as_pass)
        items = list_classifications(base_url, session=session)

        os.makedirs(os.path.dirname(output_path), exist_ok=True)
        with open(output_path, "w", encoding="utf-8") as f:
            json.dump(items, f, indent=2, ensure_ascii=False)

        feedback.pushInfo(f"Wrote {len(items)} classifications to {output_path}")

        if add_layer:
            layer = build_classifications_layer(items)
            if not layer.isValid():
                raise QgsProcessingException(
                    "Failed to create in-memory layer for classifications"
                )
            QgsProject.instance().addMapLayer(layer)
            feedback.pushInfo(
                f"Added classifications table layer with {layer.featureCount()} rows"
            )

        return {self.PARAM_OUTPUT: output_path}


class ArrayStorageDeleteClassificationAlgorithm(_BaseArrayStorageAlgorithm):
    """
    Deletes a classification by name.
    """

    PARAM_CLASSIFICATION_NAME = "CLASSIFICATION_NAME"
    PARAM_OUTPUT = "OUTPUT_JSON"

    def name(self) -> str:
        return "arraystorage_delete_classification"

    def displayName(self) -> str:
        return self.tr("Delete Classification")

    def shortHelpString(self) -> str:
        return self.tr(
            "Calls DELETE /rest/arrayStorage/classifications/{name} to remove a classification."
        )

    def icon(self):
        plugin_path = os.path.dirname(__file__)
        return QIcon(os.path.join(plugin_path, "icon_arraystore_delete.png"))

    def classification_name_param(self):
        return QgsProcessingParameterString(
            self.PARAM_CLASSIFICATION_NAME,
            self.tr("Classification name"),
        )

    def output_param(self):
        return QgsProcessingParameterFileDestination(
            self.PARAM_OUTPUT,
            self.tr("Output JSON (response, optional)"),
            fileFilter="JSON (*.json)",
            optional=True,
        )

    def initAlgorithm(self, config):
        self.addParameter(self.base_url_param())
        self.addParameter(self.as_user_param())
        self.addParameter(self.as_pass_param())
        self.addParameter(self.classification_name_param())
        self.addParameter(self.output_param())

    def processAlgorithm(self, parameters, context, feedback):
        base_url = self.parameterAsString(parameters, self.PARAM_BASE_URL, context)
        as_user = (
            self.parameterAsString(parameters, self.PARAM_AS_USER, context) or None
        )
        as_pass = (
            self.parameterAsString(parameters, self.PARAM_AS_PASS, context) or None
        )

        classification_name = self.parameterAsString(
            parameters, self.PARAM_CLASSIFICATION_NAME, context
        ).strip()
        if not classification_name:
            raise QgsProcessingException("Classification name is required")

        output_path = self.parameterAsString(parameters, self.PARAM_OUTPUT, context)
        if output_path:
            output_path = self.parameterAsFileOutput(
                parameters, self.PARAM_OUTPUT, context
            )
            os.makedirs(os.path.dirname(output_path), exist_ok=True)

        session = make_session(as_user, as_pass)
        resp = delete_classification(base_url, classification_name, session=session)

        if output_path:
            with open(output_path, "w", encoding="utf-8") as f:
                json.dump(resp, f, indent=2, ensure_ascii=False)
            feedback.pushInfo(f"Saved delete_classification response to {output_path}")

        return {self.PARAM_OUTPUT: output_path or ""}


class ArrayStorageBuildClassificationLayersAlgorithm(_BaseArrayStorageAlgorithm):
    """
    Creates dummy raster layers for each classification so symbology can be edited.
    """

    PARAM_NAMES_FILTER = "CLASSIFICATION_NAMES"

    def name(self) -> str:
        return "arraystorage_build_classification_layers"

    def displayName(self) -> str:
        return self.tr("Load Classifications as Dummy Layers")

    def shortHelpString(self) -> str:
        return self.tr(
            "Fetches classifications and creates 1x1 dummy raster layers with the "
            "corresponding symbology applied. Layers are placed into the group "
            "'ArrayStorage - Classifications' so they can be edited and later uploaded."
        )

    def icon(self):
        plugin_path = os.path.dirname(__file__)
        return QIcon(os.path.join(plugin_path, "icon_arraystore_download.png"))

    def names_filter_param(self):
        return QgsProcessingParameterString(
            self.PARAM_NAMES_FILTER,
            self.tr("Classification names (comma/newline separated, optional)"),
            optional=True,
            multiLine=True,
        )

    def initAlgorithm(self, config):
        self.addParameter(self.base_url_param())
        self.addParameter(self.as_user_param())
        self.addParameter(self.as_pass_param())
        self.addParameter(self.names_filter_param())

    def _split_names(self, raw: str) -> set[str]:
        names: set[str] = set()
        for part in raw.replace(",", "\n").splitlines():
            p = part.strip()
            if p:
                names.add(p)
        return names

    def processAlgorithm(self, parameters, context, feedback):
        base_url = self.parameterAsString(parameters, self.PARAM_BASE_URL, context)
        as_user = (
            self.parameterAsString(parameters, self.PARAM_AS_USER, context) or None
        )
        as_pass = (
            self.parameterAsString(parameters, self.PARAM_AS_PASS, context) or None
        )

        names_raw = self.parameterAsString(parameters, self.PARAM_NAMES_FILTER, context)
        name_filter = self._split_names(names_raw) if names_raw else None

        session = make_session(as_user, as_pass)
        items = list_classifications(base_url, session=session)

        if name_filter:
            items = [it for it in items if str(it.get("name")) in name_filter]

        if not items:
            raise QgsProcessingException("No classifications found with given filter")

        project = QgsProject.instance()
        root = project.layerTreeRoot()
        group_name = "ArrayStorage - Classifications"
        group = root.findGroup(group_name)
        if group is None:
            group = root.addGroup(group_name)

        created = 0
        for item in items:
            cls_name = str(item.get("name") or "")
            if not cls_name:
                continue

            # remove existing layer with same name in the target group
            for child in list(group.children()):
                if child.name() == cls_name and hasattr(child, "layerId"):
                    project.removeMapLayer(child.layerId())

            try:
                layer = create_dummy_raster_layer(cls_name)
                layer.setCustomProperty("arraystorage:classification_id", cls_name)
                layer.setCustomProperty("arraystorage:base_url", base_url)
                layer.setCustomProperty("arraystorage:kind", "classification_dummy")
                layer.setCustomProperty("arraystorage:metadata", json.dumps(item))

                apply_classification_to_layer(layer, item)

                project.addMapLayer(layer, False)
                group.insertLayer(0, layer)
                created += 1
            except Exception as e:  # noqa: BLE001
                feedback.pushInfo(
                    f"Skipping classification '{cls_name}': could not build layer ({e})"
                )

        feedback.pushInfo(
            f"Created {created} classification dummy layer(s) in group '{group_name}'"
        )
        return {}
