# -*- coding: utf-8 -*-
"""
***************************************************************************

 GeoDataFarm - A QGIS plugin
 This is a plugin that aims to determine the yield impact of different factors

 * begin                : 2016-05-13
 * copyright            : (C) 2016 by Axel Horteborn
 * email                : geodatafarm@gmail.com

***************************************************************************

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE.

***************************************************************************

"""
try:
    from typing import TYPE_CHECKING, Self
except ImportError:
    from qgis.PyQt.QtWidgets import QMessageBox
    QMessageBox.information(None, "Import Error",
                            "Your python version within QGIS is too old, please update to python 3.11 or higher")
if TYPE_CHECKING:
    import pytest_qgis.qgis_interface
# TODO: ensure that no calls to the database within tasks handel errors correctly
#import pydevd
#pydevd.settrace('localhost', port=53100, stdoutToServer=True, stderrToServer=True)
# Initialize Qt resources from file resources.py
# Import the code for the dialog
import os.path
from .GeoDataFarm_dockwidget import GeoDataFarmDockWidget
import sys
if sys.platform == 'win32':
    sys.path.append('C:\\OSGeo4W\\apps\\qgis\\python')
else:
    sys.path.append('/usr/lib/qgis')
    sys.path.append('/usr/share/qgis/python/plugins')
from qgis.core import QgsApplication

from qgis.PyQt import QtCore
from qgis.PyQt.QtCore import QSettings, QTranslator, qVersion, QCoreApplication
from qgis.PyQt.QtWidgets import QAction, QMessageBox, QApplication, QListWidgetItem, QScrollArea, QFrame
from qgis.PyQt.QtGui import QIcon, QImage, QPixmap

# Qt5/Qt6 compat: ensure the unscoped Yes/No aliases exist as attributes
if not hasattr(QMessageBox, 'Yes'):
    setattr(QMessageBox, 'Yes', QMessageBox.StandardButton.Yes)
    setattr(QMessageBox, 'No', QMessageBox.StandardButton.No)
from psycopg2 import IntegrityError, sql as pgsql
import os

import webbrowser
from . import resources  # noqa: F401 - registers the compiled Qt resource paths (:/plugins/GeoDataFarm/...) as an import side effect
plugin_dir = os.path.dirname(__file__)

# Import the code for the dock_widget and the subwidgets
from .database_scripts.create_new_farm import CreateFarm
from .database_scripts.db import DB
from .database_scripts.mean_analyse import Analyze
from .database_scripts.plan_ahead import PlanAhead
from .database_scripts.crop_simulation import CropSimulation
from .database_scripts.table_managment import TableManagement
from .import_data.handle_irrigation import IrrigationHandler
from .import_data.save_planting_data import SavePlanting
from .import_data.save_fertilizing_data import SaveFertilizing
from .import_data.save_spraying_data import SaveSpraying
from .import_data.save_other_data import SaveOther
from .import_data.save_harvest_data import SaveHarvesting
from .import_data.save_plowing_data import SavePlowing
from .import_data.save_harrowing_data import SaveHarrowing
from .import_data.save_soil_data import SaveSoil
from .import_data.convert_harvest_to_area import ConvertToAreas
from .import_data.satellite_data import SatelliteData
from .import_data.handle_weather import WeatherData
from .import_data.handle_text_data import InputTextHandler
from .import_data.handle_iso11783 import Iso11783
from .import_data.handle_input_shp_data import InputShpHandler
from .import_data.handle_raster import ImportRaster
from .widgets.add_data_form import AddDataForm
from .support_scripts.__init__ import isint, TR
from .support_scripts.notifier import (
    MessageBarNotifier, GeoDataFarmError, set_active_notifier,
    report_info, report_success, report_warning, report_error)
from .support_scripts.qt_data import _check_state, _item_flag
from .support_scripts.add_field import AddField
from .support_scripts.add_layer_to_canvas import AddLayerToCanvas
from .support_scripts.create_layer import CreateLayer
from .support_scripts.create_guiding_file import CreateGuideFile
from .support_scripts.find_iso_field import FindIsoField
from .support_scripts.find_shape_field import FindShapeField
from .support_scripts.fix_rows import RowFixer
from .support_scripts.drop_unreal import DropUnReal
from .support_scripts.generate_reports import RapportGen
from .support_scripts.multiedit import MultiEdit
from .support_scripts.populate_lists import Populate
from .support_scripts.rescale_values import RescaleValues
from .support_scripts.pyagriculture.generate_taskdata_commands import GenerateTaskCommands

class GeoDataFarm:
    """QGIS Plugin Implementation."""

    def __init__(self: Self, iface: "pytest_qgis.qgis_interface.QgisInterface", test_mode: bool=False) -> None:
        """Constructor.

        Parameters
        ----------
        iface: QgsInterface, An interface instance that will be passed to this class
            which provides the hook by which you can manipulate the QGIS
            application at run time.

        """
        # Save reference to the QGIS interface
        self.iface = iface

        # Single place that turns errors/warnings into message-bar entries
        # (with a 'Let us know' report button). Installed plugin-wide so any
        # module can call support_scripts.notifier.report_exception().
        self.notifier = MessageBarNotifier(iface)
        set_active_notifier(self.notifier)

        # initialize plugin directory
        self.plugin_dir = os.path.dirname(__file__)
        translate = TR()
        self.tr = translate.tr
        # initialize locale
        # QSettings().value('locale/userLocale') may be None in test environments
        if test_mode:
            locale = 'en'
        else:
            locale_val = QSettings().value('locale/userLocale')
            if isinstance(locale_val, str) and len(locale_val) >= 2:
                locale = locale_val[0:2]
            else:
                try:
                    locale = str(locale_val)[0:2]
                except Exception:
                    locale = 'en'

        locale_path = os.path.join(
            self.plugin_dir,
            'i18n',
            'GeoDataFarm_{}.qm'.format(locale))

        if os.path.exists(locale_path):
            self.translator = QTranslator()
            self.translator.load(locale_path)

            if qVersion() > '4.3.3':
                QCoreApplication.installTranslator(self.translator)

        # Declare instance attributes
        self.actions = []
        self.menu = self.tr(u'&GeoFarm')
        self.toolbar = self.iface.addToolBar(u'GeoDataFarm')
        self.toolbar.setObjectName(u'GeoDataFarm')
        # Track whether GUI (menu/toolbar) has been initialized
        self.gui_initialized = False
        self.tsk_mngr = QgsApplication.taskManager()

        #print "** INITIALIZING GeoDataFarm"
        self.items_in_table = None
        self.pluginIsActive = False
        self.dock_widget = None
        self.populate = None
        self.db = None
        self.IH = None
        self.df = None
        self.db = None
        self.add_field = None
        self.save_planting = None
        self.save_fertilizing = None
        self.save_spraying = None
        self.save_other = None
        self.save_harvesting = None
        self.save_plowing = None
        self.save_harrowing = None
        self.save_soil = None
        self.plan_ahead = None
        self.report_generator = None
        self.tabel_mgmt = None
        self.guide = None
        self.test_mode = test_mode
        self.find_iso_field = None
        # helper for opening generate dialogs/widgets
        self.generate_commands = GenerateTaskCommands(self)

    # noinspection PyMethodMayBeStatic

    def add_action(self, icon_path, text, callback, enabled_flag=True,
                   add_to_menu=True, add_to_toolbar=True, status_tip=None,
                   whats_this=None, parent=None):
        """Add a toolbar icon to the toolbar.

        Parameters
        ----------
        icon_path: str
            Path to the icon for this action. Can be a resource
            path (e.g. ':/plugins/foo/bar.png') or a normal file system path.
        text: str
            Text that should be shown in menu items for this action.
        callback: function
            Function to be called when the action is triggered.
        enabled_flag: bool
            A flag indicating if the action should be enabled
            by default. Defaults to True.
        add_to_menu: bool
            Flag indicating whether the action should also
            be added to the menu. Defaults to True.
        add_to_toolbar: bool
            Flag indicating whether the action should also
            be added to the toolbar. Defaults to True.
        status_tip: str
            Optional text to show in a popup when mouse pointer
            hovers over the action.
        parent: QWidget
            Parent widget for the new action. Defaults None.
        whats_this: str
            Optional text to show in the status bar when the
            mouse pointer hovers over the action.

        Returns
        -------
        QAction
            The action that was created. Note that the action is also
            added to self.actions list.

        """

        icon = QIcon(icon_path)
        action = QAction(icon, text, parent)
        action.triggered.connect(callback)
        action.setEnabled(enabled_flag)

        if status_tip is not None:
            action.setStatusTip(status_tip)

        if whats_this is not None:
            action.setWhatsThis(whats_this)

        if add_to_toolbar:
            self.toolbar.addAction(action)

        if add_to_menu:
            self.iface.addPluginToMenu(
                self.menu,
                action)

        self.actions.append(action)

        return action

    def initGui(self):
        """Create the menu entries and toolbar icons inside the QGIS GUI."""
        icon_path = ':/plugins/GeoDataFarm/img/icon.png'
        self.add_action(
            icon_path,
            text=self.tr(u'GeoDataFarm'),
            callback=self.run,
            parent=self.iface.mainWindow())
        # plugin action is added here.
        self.gui_initialized = True

    def onClosePlugin(self):
        """Cleanup necessary items here when plugin dock_widget is closed"""
        self.dock_widget.closingPlugin.disconnect(self.onClosePlugin)
        self.pluginIsActive = False

    def unload(self):
        """Removes the plugin menu item and icon from QGIS GUI."""
        #print "** UNLOAD GeoDataFarm"

        for action in self.actions:
            self.iface.removePluginMenu(
                self.tr(u'&GeoFarm'),
                action)
            self.iface.removeToolBarIcon(action)
        # remove the toolbar
        del self.toolbar

    def _log_exception(self, message: str, exception: Exception) -> None:
        """Log an exception and show it to the user with report buttons.

        Routes through the notifier so the user gets a message-bar entry with
        'Open logs' and 'Let us know' (report an issue) buttons instead of a
        silent log line.

        Parameters
        ----------
        message: str
            Context describing what was being attempted.
        exception: Exception
            The exception that was raised.
        """
        error = GeoDataFarmError(log_message=message)
        error.__cause__ = exception
        self.notifier.display_exception(error)


    # Functions create specific for GeoDataFarm-----------------------------
    def add_selected_tables(self):
        """Adds a layer for each "parameter" of all selected tables"""
        add_layer_to_canvas = AddLayerToCanvas(self)
        add_layer_to_canvas.run()

    def add_iso_field(self):
        self.find_iso_field = FindIsoField(self)
        self.find_iso_field.run()

    def add_shape_field(self):
        self.find_shape_field = FindShapeField(self)
        if not self.test_mode:
            self.find_shape_field.run()

    def reload_layer(self):
        """Reloads a layer be create a CreateLayer object and call
        the function repaint_layer"""
        create_layer = CreateLayer(self.db, self.dock_widget)
        create_layer.repaint_layer()

    def reload_range(self: Self) -> None:
        """Reload the range of the lowest and highest value of the layer"""
        cb = self.dock_widget.mMapLayerComboBox
        layer = cb.currentLayer()
        if layer is not None:
            try:
                lv = layer.renderer().ranges()[0].lowerValue()
                hv = layer.renderer().ranges()[-1].upperValue()
                nbr_cat = len(layer.renderer().ranges())
                self.dock_widget.LEMinColor.setText(str(lv))
                self.dock_widget.LEMaxColor.setText(str(hv))
                self.dock_widget.LEMaxNbrColor.setText(str(nbr_cat))
            except AttributeError:
                pass

    def run_analyse(self):
        """Gathers the parameters and start the analyse dialog"""
        names = []
        harvest_file = False
        input_file = False
        self.items_in_table = self.populate.get_items_in_table()
        self.lw_list = self.populate.get_lw_list()
        for i, (lw, schema) in enumerate(self.lw_list):
            for item in (self.items_in_table[i][0] or []):
                if (item.checkState() == 2 and
                        self.db.check_table_exists(item.text(), schema, False)):
                    if schema == 'harvest':
                        harvest_file = True
                    if schema == 'plant' or schema == 'soil' or schema == 'spray' or schema == 'ferti' or schema == 'weather':
                        input_file = True
                    names.append([schema, item.text()])
        if harvest_file and input_file:
            analyse = Analyze(self, names)
            if analyse.check_consistency():
                analyse.default_layout()
                analyse.run()
            else:
                return
        else:
            if self.test_mode:
                return False
            else:
                report_warning(self.tr('You need to have at least one input (activity or soil) and one harvest data set selected.'))

    def _q_replace_db_data(self, tbl=None):
        """Function that might be removed after the full support for shape files
        """
        schema = self.dock_widget.CBDataType.currentText()
        tables_in_db = self.db.get_tables_in_db(schema=schema)
        if tbl is not None:
            tbl_name = tbl
        else:
            tbl_name = str(self.IH.file_name)
        if isint(tbl_name[0]):
            tbl_name = '_' + tbl_name
        if tbl_name in tables_in_db:
            if self.test_mode:
                return False
            else:
                qm = QMessageBox()
                ret = qm.question(None, 'Message',
                                self.tr("The name of the data set already exist in your database, would you like to replace it?"),
                                qm.Yes, qm.No)
                if ret == qm.No:
                    return False
                else:
                    from psycopg2 import sql as pgsql
                    query = pgsql.SQL("DROP TABLE {schema}.{tbl}").format(
                        schema=pgsql.Identifier(schema),
                        tbl=pgsql.Identifier(tbl_name))
                    self.db.execute_sql(query)
                    return True
        else:
            return True

    def tbl_mgmt(self: Self) -> None:
        """Open the table manager widget"""
        self.tabel_mgmt = TableManagement(self)
        self.tabel_mgmt.run()

    def multi_edit(self):
        """Opens the multi edit widget"""
        me = MultiEdit(self)
        me.show()

    def drop_unreal(self):
        du = DropUnReal(self)
        du.show()

    def rescale_values(self):
        rv = RescaleValues(self)
        rv.show()

    def import_irrigation(self):
        """Opens the irrigation handler widget"""
        irr = IrrigationHandler(self)
        irr.run()


    def get_database_connection(self: Self) -> bool:
        """Connects to the database and create the db object"""
        self.db = DB(self.dock_widget, path=self.plugin_dir, test_mode=self.test_mode)
        if self.test_mode:
            return False
        connected = self.db.set_conn()
        if not connected:
            report_info(self.tr("Welcome to GeoDataFarm, this is a plugin still under development, if you have any suggestions of imporvements or don't understand some parts please do send a e-mail to me at geodatafarm@gmail.com"))
            return False
        return True

    def add_crop(self: Self) -> None:
        """Adds a crop to the database"""
        crop_name = self.dock_widget.LECropName.text()
        if len(crop_name) == 0:
            if self.test_mode:
                return False
            else:
                report_warning(self.tr('Crop name must be filled in.'))
                return
        sql = "Insert into crops (crop_name) VALUES (%s)"
        r_value = self.db.execute_sql(sql, params=(crop_name,), return_failure=True)
        if r_value is IntegrityError:
            if self.test_mode:
                return False
            else:
                report_warning(self.tr('Crop name already exist, please select a new name'))
                return
        _name = QApplication.translate("qadashboard", crop_name, None)
        item = QListWidgetItem(_name, self.dock_widget.LWCrops)
        item.setFlags(item.flags() | _item_flag('ItemIsUserCheckable'))
        item.setCheckState(_check_state('Unchecked'))
        self.populate.reload_crops()

    def remove_crop_name(self: Self) -> None:
        for i in range(self.dock_widget.LWCrops.count()):
            item = self.dock_widget.LWCrops.item(i)
            if item.checkState() == 2:
                sql = "delete from crops where crop_name = %s"
                self.db.execute_sql(sql, params=(item.text(),))
        self.populate.reload_crops()

    def clicked_create_farm(self: Self) -> CreateFarm:
        """Connects the docked widget with the CreateFarm script and starts
        the create_farm widget"""
        create_farm = CreateFarm(self, True)
        if not self.test_mode:
            create_farm.run()
        else:
            return create_farm

    def connect_to_farm(self):
        """Connects the docked widget with the CreateFarm script and starts
        the connect to farm widget"""
        create_farm = CreateFarm(self, False)
        if not self.test_mode:
            create_farm.run()
        else:
            return create_farm

    def fix_rows(self):
        RowFixer(self)

    def run_interpolate_harvest(self):
        cta = ConvertToAreas(self)
        cta.run()

    def _get_generate_tab(self):
        """Get the 'Generate ISO XMLs' tab."""
        dock = self.dock_widget
        return getattr(dock, 'tab_generate_isoxml', None) or getattr(dock, 'tab_17', None)

    def open_generate_menu(self):
        """Show the Generate ISO XMLs tab."""
        tab = self._get_generate_tab()
        if tab is None:
            return

        # Select the tab if possible
        try:
            tab_widget = getattr(self.dock_widget, 'tabWidget', None)
            if tab_widget:
                idx = tab_widget.indexOf(tab)
                if idx != -1:
                    tab_widget.setCurrentIndex(idx)
        except Exception as e:
            self._log_exception("Failed to select tab", e)

        # Show dock
        try:
            self.dock_widget.show()
            self.dock_widget.raise_()
        except Exception as e:
            self._log_exception("Failed to show dock", e)

    def set_buttons(self: Self) -> None:
        """Since most functions are dependent on that a database connections
        exist the buttons are set when a connection is set. If new connections
        are added here do not forget to add them in create_new_farms function
        that resets the database connection"""
        if self.populate is None:
            # Config-driven shared Add-data form. The 'Add data' tab is declared
            # in GeoDataFarm_dockwidget_base.ui (as the empty 'layoutAddData'
            # placeholder); here we just drop the form widget into it. Created
            # before Populate so its field/crop combos get filled too.
            if getattr(self, 'add_data_form', None) is None:
                self.add_data_form = AddDataForm()
                self.add_data_form.save_callback = self.save_add_data
                self.add_data_form.import_callback = self.import_add_data
                self.add_data_form.picker_action_callback = self.handle_add_data_action
            self.dock_widget.add_data_form = self.add_data_form
            if self.dock_widget.layoutAddData.count() == 0:
                self.dock_widget.layoutAddData.addWidget(self.add_data_form)
            # Crop simulation tab (Pro): same "build in Python, drop into an
            # empty .ui placeholder" pattern as Add data above - created
            # before Populate so its field combo gets filled too.
            if getattr(self, 'crop_simulation', None) is None:
                self.crop_simulation = CropSimulation(self)
            self.dock_widget.crop_simulation_page = self.crop_simulation.page
            if self.dock_widget.layoutCropSimulation.count() == 0:
                self.dock_widget.layoutCropSimulation.addWidget(self.crop_simulation.page)
            self.populate = Populate(self)
            self.dock_widget.PBOpenRD.clicked.connect(self.import_irrigation)
            self.dock_widget.PBUpdateLists.clicked.connect(self.populate.update_table_list)
            self.save_planting = SavePlanting(self)
            self.satellite_data = SatelliteData(self)
            self.satellite_data.set_widget_connections()
            self.weather_data = WeatherData(self)
            self.weather_data.set_widget_connections()
            self.crop_simulation.set_widget_connections()
            self.save_planting.set_widget_connections()
            self.save_fertilizing = SaveFertilizing(self)
            self.save_fertilizing.set_widget_connections()
            self.report_generator = RapportGen(self)
            self.report_generator.set_widget_connections()
            self.add_field = AddField(self)
            self.add_field.set_widget_connections()
            self.plan_ahead = PlanAhead(self)
            self.plan_ahead.set_widget_connections()
            self.save_spraying = SaveSpraying(self)
            self.save_spraying.set_widget_connections()
            self.save_other = SaveOther(self)
            self.save_other.set_widget_connections()
            self.save_harvesting = SaveHarvesting(self)
            self.save_harvesting.set_widget_connections()
            self.save_plowing = SavePlowing(self)
            self.save_plowing.set_widget_connections()
            self.save_harrowing = SaveHarrowing(self)
            self.save_harrowing.set_widget_connections()
            self.save_soil = SaveSoil(self)
            self.save_soil.set_widget_connections()
            self.dock_widget.PBAddCrop.clicked.connect(self.add_crop)
            self.dock_widget.PBAddIsoField.clicked.connect(self.add_iso_field)
            self.dock_widget.PBAddShapeField.clicked.connect(self.add_shape_field)
            self.dock_widget.PBRemoveCrop.clicked.connect(self.remove_crop_name)
            self.dock_widget.PBMultiEdit.clicked.connect(self.multi_edit)
            self.dock_widget.PBDropUnReal.clicked.connect(self.drop_unreal)
            self.dock_widget.PBRescaleValues.clicked.connect(self.rescale_values)
            self.dock_widget.PBReloadLayer.clicked.connect(self.reload_layer)
            self.dock_widget.PBEditTables.clicked.connect(self.tbl_mgmt)
            # Embed the guide-file wizard as the 'Guide file' sidebar tab.
            if getattr(self, 'guide', None) is None:
                self.guide = CreateGuideFile(self)
                self.guide.setup()
            if self.dock_widget.layoutGuideFile.count() == 0:
                _gf_scroll = QScrollArea()
                _gf_scroll.setWidgetResizable(True)
                _gf_scroll.setFrameShape(
                    QFrame.Shape.NoFrame if hasattr(QFrame, 'Shape')
                    else getattr(QFrame, 'NoFrame'))
                _gf_scroll.setWidget(self.guide.CGF)
                self.dock_widget.layoutGuideFile.addWidget(_gf_scroll)
            self.dock_widget.PBFixRows.clicked.connect(self.fix_rows)
            self.dock_widget.PBRunAnalyses.clicked.connect(self.run_analyse)
            self.dock_widget.PBAdd2Canvas.clicked.connect(self.add_selected_tables)
            self.dock_widget.PBWebbpage.clicked.connect(lambda: webbrowser.open('http://www.geodatafarm.com/'))
            self.dock_widget.PBSponsor.clicked.connect(lambda: webbrowser.open('https://github.com/sponsors/axelande'))
            self.dock_widget.PBHvInterpolateData.clicked.connect(self.run_interpolate_harvest)

    def save_add_data(self):
        """Generic, config-driven manual save for the shared Add-data form.

        Reads the open operation's config + the form values (keyed by DB
        column) and builds the same INSERT the old per-operation handlers did.
        """
        form = self.add_data_form
        cfg = form.config
        if cfg is None:
            return
        if cfg.get('custom_save') == 'other':
            self._save_other_from_form()
            return
        if cfg.get('special'):
            report_info(self.tr(
                'This operation is not yet available in the new Add data form.'))
            return
        v = form.values()
        if not v['field'] or v['field'] == self.tr('--- Select field ---'):
            report_warning(self.tr('In order to save the data you must select a field'))
            return
        if cfg['needs_crop'] and (not v.get('crop') or v['crop'] == self.tr('--- Select crop ---')):
            report_warning(self.tr('In order to save the data you must select a crop'))
            return
        cols = (['field'] + (['crop'] if cfg['needs_crop'] else []) + ['date_']
                + [c for _, c, _ in cfg['fields']] + ['other'])
        params = ([v['field']] + ([v['crop']] if cfg['needs_crop'] else []) + [v['date']]
                  + [v[c] for _, c, _ in cfg['fields']] + [v['other']])
        if cfg.get('table_none'):
            cols.append('table_')
            params.append('None')
        # cfg['table'] is schema-qualified (e.g. "plant.manual"); compose the
        # table and column identifiers safely and keep values parameterized.
        table_id = pgsql.SQL('.').join(
            pgsql.Identifier(part) for part in cfg['table'].split('.'))
        sql = pgsql.SQL("INSERT INTO {} ({}) VALUES ({})").format(
            table_id,
            pgsql.SQL(', ').join(pgsql.Identifier(c) for c in cols),
            pgsql.SQL(', ').join(pgsql.Placeholder() for _ in params))
        try:
            self.db.execute_sql(sql, params=tuple(params))
            report_success(self.tr('The data was stored correctly'))
            form.clear()
        except Exception as e:
            report_error(self.tr('Following error occurred: {}').format(e),
                         detail=str(e))

    def import_add_data(self, key):
        """Generic, config-driven file import for the shared Add-data form.

        Opens the same importer dialog the old per-operation handlers used,
        dispatched on the file type's stable ``key`` (from the clicked button).
        """
        form = self.add_data_form
        cfg = form.config
        if cfg is None or cfg.get('special'):
            return
        if key == 'db':
            report_info(self.tr(
                'Support for databasefiles are not implemented 100% yet'))
            return
        columns = [self.tr(c) for c in cfg.get('import_columns', [])]
        schema = cfg.get('schema')
        if key == 'text':
            InputTextHandler(self, schema, columns=columns).run()
        elif key == 'iso':
            Iso11783(self, schema).run()
        elif key == 'shp':
            InputShpHandler(self, cfg.get('shp_schema', schema), columns).run()
        elif key == 'raster':
            ImportRaster(self, form.dateWhen, form.cbField, schema).run()

    def handle_add_data_action(self, op):
        """Card-click actions for operations without a manual form."""
        if op == 'irrigation':
            self.import_irrigation()
        elif op == 'weather':
            self.weather_data.open_dialog()

    def _save_other_from_form(self):
        """Custom save for the 'Other' operation (dynamic per-operation table),
        reading the shared Add-data form. Ports the old SaveOther logic."""
        from psycopg2 import sql as pgsql
        from .support_scripts.__init__ import check_text
        form = self.add_data_form
        v = form.values()
        field = v['field']
        if not field or field == self.tr('--- Select field ---'):
            report_warning(self.tr('In order to save the data you must select a field'))
            return
        crop = v.get('crop')
        if crop == self.tr('--- Select crop ---'):
            crop = None
        date_ = v['date']
        select_parts = [pgsql.SQL("%s AS field"), pgsql.SQL("%s AS crop"),
                        pgsql.SQL("%s AS date_")]
        params = [field, crop, date_]
        for opt_key, unit_key, val_key in [('opt1', 'unit1', 'val1'),
                                           ('opt2', 'unit2', 'val2'),
                                           ('opt3', 'unit3', 'val3'),
                                           ('opt4', 'unit4', 'val4')]:
            option = check_text(v.get(opt_key) or '')
            if option == '':
                continue
            unit = check_text(v.get(unit_key) or '')
            if unit == '':
                unit = 'Null'
            value = check_text(v.get(val_key) or '')
            if value == '':
                continue
            select_parts.append(pgsql.SQL("%s AS {alias}").format(
                alias=pgsql.Identifier(f"{option}_{unit}")))
            params.append(value)
        select_parts.append(pgsql.SQL("%s AS other"))
        params.append(v.get('other'))
        name = v.get('other_name') or ''
        tbl = f"{check_text(name)}_{check_text(date_)}_{field}"
        exists = self.db.execute_and_return(
            "SELECT EXISTS ("
            " SELECT 1 FROM information_schema.tables"
            " WHERE table_schema = 'other' AND table_name = %s)",
            params=(tbl,))[0][0]
        if exists:
            report_warning(self.tr(
                'That operation, at that field on that day is already stored'))
            return
        query = pgsql.SQL("SELECT {cols} INTO other.{tbl}").format(
            cols=pgsql.SQL(", ").join(select_parts), tbl=pgsql.Identifier(tbl))
        try:
            self.db.execute_sql(query, params=tuple(params))
            report_success(self.tr('The data was stored correctly'))
            form.clear()
        except Exception as e:
            report_error(self.tr('Following error occurred: {}').format(e),
                         detail=str(e))

    def create_guide(self: Self) -> None:
        """(Re)create and wire the guide-file controller. The wizard is embedded
        as the 'Guide file' sidebar tab (set up in set_buttons); this is kept
        for API/test compatibility and refreshes self.guide with the current
        field list."""
        self.guide = CreateGuideFile(self)
        self.guide.setup()

    def run(self: Self, test_mode: bool=False) -> None:
        """Run method that loads and starts the plugin"""
        # Ensure GUI items are initialized (useful when calling `run()` directly)
        if not getattr(self, 'gui_initialized', False):
            try:
                self.initGui()
            except Exception:  # nosec B110
                # best-effort: if initGui fails (e.g., headless tests), continue
                pass
        icon_path = ':/plugins/GeoDataFarm/img/icon.png'
        if not self.pluginIsActive:
            self.pluginIsActive = True

            #print "** STARTING GeoDataFarm"

            # dock_widget may not exist if:
            #    first run of plugin
            #    removed on close (see self.onClosePlugin method)
            if self.dock_widget is None:
                # Create the dock_widget (after translation) and keep reference
                self.dock_widget = GeoDataFarmDockWidget()

                # From now on, plugin messages show in the dock's own
                # message bar instead of the main QGIS window's - see
                # notifier.MessageBarNotifier.set_dock_message_bar.
                self.notifier.set_dock_message_bar(self.dock_widget.message_bar)

                # Set the parent_gdf reference for GenerateTaskDataWidget
                if hasattr(self.dock_widget, 'generate_taskdata_widget'):
                    self.dock_widget.generate_taskdata_widget.parent_gdf = self

                # Set the parent_gdf reference for GenerateIsoxmlController
                if hasattr(self.dock_widget, 'generate_isoxml_controller'):
                    self.dock_widget.generate_isoxml_controller.parent_gdf = self

                img = QImage(icon_path)
                
                if hasattr(QtCore.Qt, "AspectRatioMode"):  # Qt6+
                    KeepAspectRatio = QtCore.Qt.AspectRatioMode.KeepAspectRatio
                    SmoothTransformation = QtCore.Qt.TransformationMode.SmoothTransformation
                else:  # Qt5
                    KeepAspectRatio = getattr(QtCore.Qt, 'KeepAspectRatio')
                    SmoothTransformation = getattr(QtCore.Qt,
                                                   'SmoothTransformation')

                # Now your original line, compatible with both:
                pimg = QPixmap.fromImage(img).scaled(91, 91, KeepAspectRatio, SmoothTransformation)

                self.dock_widget.LIcon.setPixmap(pimg)
            if self.get_database_connection():
                self.set_buttons()
            # Setup the Generate ISO XMLs tab (buttons, tables)
            # Removed: _setup_generate_isoxml_tab() - now using promoted widget in UI
            self.dock_widget.PBAddNewFarm.clicked.connect(self.clicked_create_farm)
            self.dock_widget.PBConnect2Farm.clicked.connect(self.connect_to_farm)
            try:
                self.reload_range()
            except Exception:  # nosec B110
                pass
            self.dock_widget.mMapLayerComboBox.currentIndexChanged.connect(self.reload_range)
            # show the dock_widget
            # connect to provide cleanup on closing of dock_widget
            self.dock_widget.closingPlugin.connect(self.onClosePlugin)
            #self.iface.addDockWidget(Qt.RightDockWidgetArea, self.dock_widget)
            self.dock_widget.show()
