"""Runtime workflow methods for PricingApp."""

from __future__ import annotations

import inspect
import json
import os
import platform
import subprocess
import tempfile
import time
from pathlib import Path
from typing import Any, Callable, Dict, Generator, Iterable, Optional, Sequence

class AppControllerRuntimeMixin:
    def run_workflow_config_ui(self, workflow_config_json: str) -> Generator[tuple[str, str], None, None]:
        """Run plotting/prediction/compare/pre-oneway from workflow JSON config."""
        try:
            if workflow_config_json:
                workflow_config = json.loads(workflow_config_json)
                base_dir = self._default_base_dir(self.current_workflow_config_dir)
                self.current_workflow_config = workflow_config
            elif self.current_workflow_config:
                workflow_config = self.current_workflow_config
                base_dir = self._default_base_dir(self.current_workflow_config_dir)
            else:
                yield "No workflow configuration provided", ""
                return

            mode = str(workflow_config.get("workflow", workflow_config).get("mode", "unknown")).strip().lower()
            runner = self._get_runner()
            log_generator = runner.run_callable(
                self._run_workflow_from_config,
                workflow_config,
                base_dir,
            )

            full_log = ""
            for log_line in log_generator:
                full_log += log_line + "\n"
                yield f"Workflow [{mode}] in progress...", full_log

            yield f"Workflow [{mode}] completed!", full_log

        except Exception as e:
            error_msg = f"Workflow config execution error: {str(e)}"
            yield error_msg, error_msg

    def run_training(self, config_json: str) -> Generator[tuple[str, str], None, None]:
        """
        Run task (training, explain, plotting, etc.) with the current configuration.

        The task type is automatically detected from config.runner.mode.
        Supported modes: entry (training), explain, incremental, watchdog, etc.
        """
        temp_config_path: Optional[Path] = None
        try:
            if config_json:
                config = json.loads(config_json)
                task_mode = config.get('runner', {}).get('mode', 'entry')
                base_dir = self._default_base_dir(self.current_config_dir)
                fd, temp_path = tempfile.mkstemp(prefix="temp_config_", suffix=".json", dir=base_dir)
                temp_config_path = Path(temp_path)
                with os.fdopen(fd, 'w', encoding='utf-8') as f:
                    json.dump(config, f, indent=2)
                config_path = temp_config_path
            elif self.current_config_path and self.current_config_path.exists():
                config_path = self.current_config_path
                config = json.loads(config_path.read_text(encoding="utf-8"))
                task_mode = config.get('runner', {}).get('mode', 'entry')
            elif self.current_config:
                config = self.current_config
                task_mode = config.get('runner', {}).get('mode', 'entry')
                base_dir = self._default_base_dir(self.current_config_dir)
                fd, temp_path = tempfile.mkstemp(prefix="temp_config_", suffix=".json", dir=base_dir)
                temp_config_path = Path(temp_path)
                with os.fdopen(fd, 'w', encoding='utf-8') as f:
                    json.dump(config, f, indent=2)
                config_path = temp_config_path
            else:
                yield "No configuration provided", ""
                return

            runner = self._get_runner()
            log_generator = runner.run_task(str(config_path))

            # Collect logs
            full_log = ""
            for log_line in log_generator:
                full_log += log_line + "\n"
                yield f"Task [{task_mode}] in progress...", full_log

            yield f"Task [{task_mode}] completed!", full_log

        except Exception as e:
            error_msg = f"Error during task execution: {str(e)}"
            yield error_msg, error_msg
        finally:
            if temp_config_path is not None:
                try:
                    temp_config_path.unlink(missing_ok=True)
                except Exception:
                    pass

    def prepare_ft_step1(self, config_json: str, use_ddp: bool, nproc: int) -> tuple[str, str]:
        """Prepare FT Step 1 configuration."""
        if not config_json:
            return "No configuration provided", ""

        try:
            config = json.loads(config_json)
            step1_config = self.ft_workflow.prepare_step1_config(
                base_config=config,
                use_ddp=use_ddp,
                nproc_per_node=int(nproc)
            )

            # Save to temp file
            base_dir = self._default_base_dir(self.current_config_dir)
            temp_path = (base_dir / "temp_ft_step1_config.json").resolve()
            with open(temp_path, 'w', encoding='utf-8') as f:
                json.dump(step1_config, f, indent=2)

            self.current_step1_config = str(temp_path)
            step1_json = json.dumps(step1_config, indent=2, ensure_ascii=False)

            return "Step 1 config prepared. Click 'Run Step 1' to train FT embeddings.", step1_json

        except Exception as e:
            return f"Error preparing Step 1 config: {str(e)}", ""

    def prepare_ft_step2(
        self,
        step1_config_path: str,
        target_models: str,
        augmented_data_dir: str,
        xgb_overrides_json: str,
        resn_overrides_json: str,
    ) -> tuple[str, str, str]:
        """Prepare FT Step 2 configurations."""
        if not step1_config_path:
            return "Step 1 config not found. Run Step 1 first.", "", ""

        try:
            step1_path = Path(step1_config_path).expanduser()
            if not step1_path.is_absolute():
                candidate_dirs: Iterable[Path] = (
                    self.current_config_dir,
                    self.working_dir,
                    Path.cwd(),
                )
                resolved_candidate = None
                for root in candidate_dirs:
                    if root is None:
                        continue
                    candidate = (Path(root).resolve() / step1_path).resolve()
                    if candidate.exists():
                        resolved_candidate = candidate
                        break
                if resolved_candidate is None:
                    return "Step 1 config not found. Run Step 1 first.", "", ""
                step1_path = resolved_candidate
            else:
                step1_path = step1_path.resolve()
                if not step1_path.exists():
                    return "Step 1 config not found. Run Step 1 first.", "", ""

            models = [m.strip() for m in target_models.split(',') if m.strip()]
            data_dir_value = str(augmented_data_dir or "").strip() or "./DataFTUnsupervised"
            xgb_overrides = self._parse_json_dict(
                xgb_overrides_json,
                "xgb_overrides_json",
            )
            resn_overrides = self._parse_json_dict(
                resn_overrides_json,
                "resn_overrides_json",
            )
            xgb_cfg, resn_cfg = self.ft_workflow.generate_step2_configs(
                step1_config_path=str(step1_path),
                target_models=models,
                augmented_data_dir=data_dir_value,
                xgb_overrides=xgb_overrides,
                resn_overrides=resn_overrides,
            )
            save_dir = step1_path.parent
            saved_paths: Dict[str, str] = {}

            if xgb_cfg:
                xgb_cfg_path = save_dir / "config_xgb_from_ft_unsupervised.json"
                xgb_cfg_path.write_text(
                    json.dumps(xgb_cfg, indent=2, ensure_ascii=False),
                    encoding="utf-8",
                )
                saved_paths["xgb"] = str(xgb_cfg_path)

            if resn_cfg:
                resn_cfg_path = save_dir / "config_resn_from_ft_unsupervised.json"
                resn_cfg_path.write_text(
                    json.dumps(resn_cfg, indent=2, ensure_ascii=False),
                    encoding="utf-8",
                )
                saved_paths["resn"] = str(resn_cfg_path)

            self.current_step2_config_paths = saved_paths

            status_lines = [
                f"Step 2 configs prepared for: {', '.join(models)}",
                f"Augmented data dir: {data_dir_value}",
            ]
            if "xgb" in saved_paths:
                status_lines.append(f"Saved XGB config: {saved_paths['xgb']}")
            if "resn" in saved_paths:
                status_lines.append(f"Saved ResN config: {saved_paths['resn']}")
            status_msg = "\n".join(status_lines)
            xgb_json = json.dumps(
                xgb_cfg, indent=2, ensure_ascii=False) if xgb_cfg else ""
            resn_json = json.dumps(
                resn_cfg, indent=2, ensure_ascii=False) if resn_cfg else ""

            return status_msg, xgb_json, resn_json

        except FileNotFoundError as e:
            return f"Error: {str(e)}\n\nMake sure Step 1 completed successfully.", "", ""
        except Exception as e:
            return f"Error preparing Step 2 configs: {str(e)}", "", ""

    def open_results_folder(self, config_json: str) -> str:
        """Open the results folder in file explorer."""
        try:
            if config_json:
                config = json.loads(config_json)
                output_dir = config.get('output_dir', './Results')
                out = Path(str(output_dir))
                if out.is_absolute():
                    results_path = out.resolve()
                else:
                    results_path = (self._default_base_dir(self.current_config_dir) / out).resolve()
            elif self.current_config_path and self.current_config_path.exists():
                config = json.loads(
                    self.current_config_path.read_text(encoding="utf-8"))
                output_dir = config.get('output_dir', './Results')
                results_path = (
                    self.current_config_path.parent / output_dir).resolve()
            elif self.current_config:
                output_dir = self.current_config.get('output_dir', './Results')
                out = Path(str(output_dir))
                if out.is_absolute():
                    results_path = out.resolve()
                else:
                    results_path = (self._default_base_dir(self.current_config_dir) / out).resolve()
            else:
                return "No configuration loaded"

            if not results_path.exists():
                return f"Results folder does not exist yet: {results_path}"

            # Open folder based on OS
            system = platform.system()
            if system == "Windows":
                os.startfile(results_path)
            elif system == "Darwin":  # macOS
                subprocess.run(["open", str(results_path)])
            else:  # Linux
                subprocess.run(["xdg-open", str(results_path)])

            return f"Opened folder: {results_path}"

        except Exception as e:
            return f"Error opening folder: {str(e)}"

    def _run_workflow(self, label: str, func: Callable, *args, **kwargs):
        """Run a workflow function and stream logs."""
        try:
            runner = self._get_runner()
            log_generator = runner.run_callable(func, *args, **kwargs)
            full_log = ""
            for log_line in log_generator:
                full_log += log_line + "\n"
                yield f"{label} in progress...", full_log
            yield f"{label} completed!", full_log
        except Exception as e:
            error_msg = f"{label} error: {str(e)}"
            yield error_msg, error_msg

    def _run_workflow_with_preview(
        self,
        label: str,
        func: Callable,
        preview_collector: Callable[[float], list[str]],
        *args,
        **kwargs,
    ):
        """Run a workflow and return status/logs plus generated image previews."""
        try:
            runner = self._get_runner()
            started_at = time.time()
            log_generator = runner.run_callable(func, *args, **kwargs)
            full_log = ""
            for log_line in log_generator:
                full_log += log_line + "\n"
                yield f"{label} in progress...", full_log, []

            previews: list[str] = []
            try:
                previews = preview_collector(started_at)
            except Exception as preview_exc:
                full_log += f"[Warn] Failed to collect image previews: {preview_exc}\n"

            yield f"{label} completed!", full_log, previews
        except Exception as e:
            error_msg = f"{label} error: {str(e)}"
            yield error_msg, error_msg, []

    def _load_json_file(self, path_value: str) -> tuple[Path, Dict[str, Any]]:
        path_obj = self._resolve_user_path(path_value, base_dir=self.working_dir)
        if not path_obj.exists():
            raise FileNotFoundError(f"Config file not found: {path_obj}")
        payload = json.loads(path_obj.read_text(encoding="utf-8"))
        if not isinstance(payload, dict):
            raise ValueError(f"Config must be a JSON object: {path_obj}")
        return path_obj, payload

    @staticmethod
    def _resolve_cfg_output_dir(cfg_obj: Dict[str, Any], cfg_path: Path) -> Path:
        raw_output = str(cfg_obj.get("output_dir", "./Results") or "./Results").strip()
        out_path = Path(raw_output).expanduser()
        if not out_path.is_absolute():
            out_path = (cfg_path.parent / out_path).resolve()
        else:
            out_path = out_path.resolve()
        return out_path

    @staticmethod
    def _collect_png_paths(
        candidates: Iterable[Path],
        *,
        min_mtime: Optional[float] = None,
        limit: int = 40,
    ) -> list[str]:
        ranked: list[tuple[float, str]] = []
        seen: set[str] = set()
        for candidate in candidates:
            path_obj = Path(candidate).resolve()
            if not path_obj.exists():
                continue
            if path_obj.is_file():
                file_iter = [path_obj] if path_obj.suffix.lower() == ".png" else []
            else:
                file_iter = path_obj.rglob("*.png")
            for file_path in file_iter:
                if not file_path.is_file():
                    continue
                resolved = str(file_path.resolve())
                if resolved in seen:
                    continue
                seen.add(resolved)
                try:
                    mtime = file_path.stat().st_mtime
                except OSError:
                    continue
                ranked.append((mtime, resolved))

        ranked.sort(key=lambda x: x[0], reverse=True)
        if min_mtime is not None:
            recent = [path for mtime, path in ranked if mtime >= min_mtime]
            if recent:
                return recent[:limit]
        return [path for _, path in ranked[:limit]]

    def _collect_prediction_plot_images(
        self,
        *,
        cfg_path: str,
        xgb_cfg_path: str,
        resn_cfg_path: str,
        started_at: float,
    ) -> list[str]:
        cfg_file, cfg_obj = self._load_json_file(cfg_path)
        xgb_file, xgb_obj = self._load_json_file(xgb_cfg_path)
        resn_file, resn_obj = self._load_json_file(resn_cfg_path)

        model_list = cfg_obj.get("model_list") or []
        model_categories = cfg_obj.get("model_categories") or []
        model_name = ""
        if model_list and model_categories:
            model_name = f"{model_list[0]}_{model_categories[0]}"

        candidate_roots: list[Path] = []
        for file_path, cfg_item in (
            (cfg_file, cfg_obj),
            (xgb_file, xgb_obj),
            (resn_file, resn_obj),
        ):
            output_root = self._resolve_cfg_output_dir(cfg_item, file_path)
            plot_root = output_root / "plot"
            candidate_roots.append(plot_root)
            if model_name:
                candidate_roots.append(plot_root / model_name)

        return self._collect_png_paths(
            candidate_roots,
            min_mtime=started_at - 1.0,
        )

    def _collect_pre_oneway_images(
        self,
        *,
        data_path: str,
        train_data_path: str,
        test_data_path: str,
        model_name: str,
        output_dir: str,
        started_at: float,
    ) -> list[str]:
        if str(output_dir or "").strip():
            out_dir = self._resolve_user_path(output_dir, base_dir=self.working_dir)
        else:
            base_path: Optional[Path] = None
            if str(train_data_path or "").strip():
                base_path = self._resolve_user_path(train_data_path, base_dir=self.working_dir)
            elif str(test_data_path or "").strip():
                base_path = self._resolve_user_path(test_data_path, base_dir=self.working_dir)
            elif str(data_path or "").strip():
                base_path = self._resolve_user_path(data_path, base_dir=self.working_dir)
            root_dir = base_path.parent if base_path is not None else self.working_dir
            out_dir = (root_dir / "Results" / "plot" / model_name / "oneway" / "pre").resolve()
        return self._collect_png_paths([out_dir], min_mtime=started_at - 1.0)

    def _collect_compare_images(
        self,
        *,
        direct_cfg_path: str,
        started_at: float,
    ) -> list[str]:
        cfg_file, cfg_obj = self._load_json_file(direct_cfg_path)
        output_root = self._resolve_cfg_output_dir(cfg_obj, cfg_file)
        plot_root = output_root / "plot"

        model_list = cfg_obj.get("model_list") or []
        model_categories = cfg_obj.get("model_categories") or []
        candidates: list[Path] = [plot_root]
        if model_list and model_categories:
            model_name = f"{model_list[0]}_{model_categories[0]}"
            candidates.append(plot_root / model_name / "double_lift")

        return self._collect_png_paths(candidates, min_mtime=started_at - 1.0)

    def _collect_double_lift_images(
        self,
        *,
        data_path: str,
        output_path: str,
        started_at: float,
    ) -> list[str]:
        raw_output_path = str(output_path or "").strip()
        if raw_output_path:
            target_path = self._resolve_user_path(raw_output_path, base_dir=self.working_dir)
            candidates = [target_path]
        else:
            data_obj = self._resolve_user_path(data_path, base_dir=self.working_dir)
            candidates = [(data_obj.parent / "Results" / "plot").resolve()]
        return self._collect_png_paths(candidates, min_mtime=started_at - 1.0)

    def run_pre_oneway_ui(
        self,
        data_path: str,
        model_name: str,
        target_col: str,
        weight_col: str,
        feature_list: str,
        oneway_factors: Optional[Sequence[str]],
        categorical_features: str,
        n_bins: int,
        holdout_ratio: float,
        rand_seed: int,
        output_dir: str,
        train_data_path: str,
        test_data_path: str,
    ):
        workflows_module = self._load_workflows_module()
        selected_factors = self._normalize_feature_values(oneway_factors)
        resolved_feature_list = ",".join(selected_factors) if selected_factors else feature_list
        yield from self._run_workflow_with_preview(
            "Pre-Oneway Plot",
            workflows_module.run_pre_oneway,
            lambda started_at: self._collect_pre_oneway_images(
                data_path=data_path,
                train_data_path=train_data_path,
                test_data_path=test_data_path,
                model_name=model_name,
                output_dir=output_dir,
                started_at=started_at,
            ),
            data_path=data_path,
            model_name=model_name,
            target_col=target_col,
            weight_col=weight_col,
            feature_list=resolved_feature_list,
            categorical_features=categorical_features,
            n_bins=n_bins,
            holdout_ratio=holdout_ratio,
            rand_seed=rand_seed,
            output_dir=output_dir or None,
            train_data_path=train_data_path or None,
            test_data_path=test_data_path or None,
        )

    def run_plot_direct_ui(
        self,
        cfg_path: str,
        xgb_cfg_path: str,
        resn_cfg_path: str,
        oneway_factors: Optional[Sequence[str]],
        train_data_path: str,
        test_data_path: str,
        xgb_model_file: Optional[Any] = None,
        resn_model_file: Optional[Any] = None,
        xgb_model_path: Optional[str] = None,
        resn_model_path: Optional[str] = None,
    ):
        workflows_module = self._load_workflows_module()
        selected_factors = self._normalize_feature_values(oneway_factors)
        resolved_xgb_model_path = self._resolve_override_path(
            xgb_model_path, xgb_model_file
        )
        resolved_resn_model_path = self._resolve_override_path(
            resn_model_path, resn_model_file
        )
        yield from self._run_workflow_with_preview(
            "Direct Plot",
            workflows_module.run_plot_direct,
            lambda started_at: self._collect_prediction_plot_images(
                cfg_path=cfg_path,
                xgb_cfg_path=xgb_cfg_path,
                resn_cfg_path=resn_cfg_path,
                started_at=started_at,
            ),
            cfg_path=cfg_path,
            xgb_cfg_path=xgb_cfg_path,
            resn_cfg_path=resn_cfg_path,
            oneway_features=selected_factors or None,
            train_data_path=train_data_path or None,
            test_data_path=test_data_path or None,
            xgb_model_path=resolved_xgb_model_path,
            resn_model_path=resolved_resn_model_path,
            model_search_dir=str(self.working_dir),
        )

    def run_plot_embed_ui(
        self,
        cfg_path: str,
        xgb_cfg_path: str,
        resn_cfg_path: str,
        ft_cfg_path: str,
        use_runtime_ft_embedding: bool,
        oneway_factors: Optional[Sequence[str]],
        train_data_path: str,
        test_data_path: str,
        xgb_model_file: Optional[Any] = None,
        resn_model_file: Optional[Any] = None,
        ft_model_file: Optional[Any] = None,
        xgb_model_path: Optional[str] = None,
        resn_model_path: Optional[str] = None,
        ft_model_path: Optional[str] = None,
    ):
        workflows_module = self._load_workflows_module()
        selected_factors = self._normalize_feature_values(oneway_factors)
        resolved_xgb_model_path = self._resolve_override_path(
            xgb_model_path, xgb_model_file
        )
        resolved_resn_model_path = self._resolve_override_path(
            resn_model_path, resn_model_file
        )
        resolved_ft_model_path = self._resolve_override_path(
            ft_model_path, ft_model_file
        )
        yield from self._run_workflow_with_preview(
            "Embed Plot",
            workflows_module.run_plot_embed,
            lambda started_at: self._collect_prediction_plot_images(
                cfg_path=cfg_path,
                xgb_cfg_path=xgb_cfg_path,
                resn_cfg_path=resn_cfg_path,
                started_at=started_at,
            ),
            cfg_path=cfg_path,
            xgb_cfg_path=xgb_cfg_path,
            resn_cfg_path=resn_cfg_path,
            ft_cfg_path=ft_cfg_path,
            use_runtime_ft_embedding=use_runtime_ft_embedding,
            oneway_features=selected_factors or None,
            train_data_path=train_data_path or None,
            test_data_path=test_data_path or None,
            xgb_model_path=resolved_xgb_model_path,
            resn_model_path=resolved_resn_model_path,
            ft_model_path=resolved_ft_model_path,
            model_search_dir=str(self.working_dir),
        )

    def run_predict_ui(
        self,
        ft_cfg_path: str,
        xgb_cfg_path: str,
        resn_cfg_path: str,
        input_path: str,
        output_path: str,
        model_name: str,
        model_keys: str,
        ft_model_file: Optional[Any] = None,
        xgb_model_file: Optional[Any] = None,
        resn_model_file: Optional[Any] = None,
        ft_model_path: Optional[str] = None,
        xgb_model_path: Optional[str] = None,
        resn_model_path: Optional[str] = None,
    ):
        workflows_module = self._load_workflows_module()
        resolved_ft_model_path = self._resolve_override_path(
            ft_model_path, ft_model_file
        )
        resolved_xgb_model_path = self._resolve_override_path(
            xgb_model_path, xgb_model_file
        )
        resolved_resn_model_path = self._resolve_override_path(
            resn_model_path, resn_model_file
        )
        yield from self._run_workflow(
            "Prediction",
            workflows_module.run_predict_ft_embed,
            ft_cfg_path=ft_cfg_path,
            xgb_cfg_path=xgb_cfg_path or None,
            resn_cfg_path=resn_cfg_path or None,
            input_path=input_path,
            output_path=output_path,
            model_name=model_name or None,
            model_keys=model_keys,
            ft_model_path=resolved_ft_model_path,
            xgb_model_path=resolved_xgb_model_path,
            resn_model_path=resolved_resn_model_path,
            model_search_dir=str(self.working_dir),
        )

    def run_compare_ui(
        self,
        model_key: str,
        direct_cfg_path: str,
        ft_cfg_path: str,
        ft_embed_cfg_path: str,
        label_direct: str,
        label_ft: str,
        use_runtime_ft_embedding: bool,
        n_bins_override: int,
        train_data_path: str,
        test_data_path: str,
        direct_model_file: Optional[Any] = None,
        ft_embed_model_file: Optional[Any] = None,
        ft_model_file: Optional[Any] = None,
        direct_model_path: Optional[str] = None,
        ft_embed_model_path: Optional[str] = None,
        ft_model_path: Optional[str] = None,
    ):
        model_key_norm = str(model_key or "").strip().lower()
        if model_key_norm not in {"xgb", "resn"}:
            raise ValueError("model_key must be one of: xgb, resn.")
        label = "Compare XGB" if model_key_norm == "xgb" else "Compare ResNet"
        resolved_direct_model_path = self._resolve_override_path(
            direct_model_path, direct_model_file
        )
        resolved_ft_embed_model_path = self._resolve_override_path(
            ft_embed_model_path, ft_embed_model_file
        )
        resolved_ft_model_path = self._resolve_override_path(
            ft_model_path, ft_model_file
        )
        yield from self._run_compare_ui(
            model_key=model_key_norm,
            label=label,
            direct_cfg_path=direct_cfg_path,
            ft_cfg_path=ft_cfg_path,
            ft_embed_cfg_path=ft_embed_cfg_path,
            label_direct=label_direct,
            label_ft=label_ft,
            use_runtime_ft_embedding=use_runtime_ft_embedding,
            n_bins_override=n_bins_override,
            train_data_path=train_data_path,
            test_data_path=test_data_path,
            direct_model_path=resolved_direct_model_path or "",
            ft_embed_model_path=resolved_ft_embed_model_path or "",
            ft_model_path=resolved_ft_model_path or "",
        )

    def _run_compare_ui(
        self,
        *,
        model_key: str,
        label: str,
        direct_cfg_path: str,
        ft_cfg_path: str,
        ft_embed_cfg_path: str,
        label_direct: str,
        label_ft: str,
        use_runtime_ft_embedding: bool,
        n_bins_override: int,
        train_data_path: str,
        test_data_path: str,
        direct_model_path: str,
        ft_embed_model_path: str,
        ft_model_path: str,
    ):
        workflows_module = self._load_workflows_module()
        yield from self._run_workflow_with_preview(
            label,
            workflows_module.run_compare_ft_embed,
            lambda started_at: self._collect_compare_images(
                direct_cfg_path=direct_cfg_path,
                started_at=started_at,
            ),
            direct_cfg_path=direct_cfg_path,
            ft_cfg_path=ft_cfg_path,
            ft_embed_cfg_path=ft_embed_cfg_path,
            model_key=model_key,
            label_direct=label_direct,
            label_ft=label_ft,
            use_runtime_ft_embedding=use_runtime_ft_embedding,
            n_bins_override=n_bins_override,
            train_data_path=train_data_path or None,
            test_data_path=test_data_path or None,
            direct_model_path=direct_model_path or None,
            ft_embed_model_path=ft_embed_model_path or None,
            ft_model_path=ft_model_path or None,
            model_search_dir=str(self.working_dir),
        )

    def run_double_lift_ui(
        self,
        data_path: str,
        pred_col_1: str,
        pred_col_2: str,
        target_col: str,
        weight_col: str,
        n_bins: int,
        label1: str,
        label2: str,
        pred1_weighted: bool,
        pred2_weighted: bool,
        actual_weighted: bool,
        holdout_ratio: float,
        split_strategy: str,
        split_group_col: str,
        split_time_col: str,
        split_time_ascending: bool,
        rand_seed: int,
        output_path: str,
    ):
        workflows_module = self._load_workflows_module()
        yield from self._run_workflow_with_preview(
            "Double Lift",
            workflows_module.run_double_lift_from_file,
            lambda started_at: self._collect_double_lift_images(
                data_path=data_path,
                output_path=output_path,
                started_at=started_at,
            ),
            data_path=data_path,
            pred_col_1=pred_col_1,
            pred_col_2=pred_col_2,
            target_col=target_col,
            weight_col=weight_col,
            n_bins=n_bins,
            label1=label1 or None,
            label2=label2 or None,
            pred1_weighted=pred1_weighted,
            pred2_weighted=pred2_weighted,
            actual_weighted=actual_weighted,
            holdout_ratio=holdout_ratio,
            split_strategy=split_strategy,
            split_group_col=split_group_col or None,
            split_time_col=split_time_col or None,
            split_time_ascending=split_time_ascending,
            rand_seed=rand_seed,
            output_path=output_path or None,
        )
