"""Run SimStadt workflows from Python.

SimStadt is located via the SIMSTADT_FOLDER environment variable, or by globbing
~/Desktop for SimStadt2_0.*/.

Workflow templates (.flow directories) are located via the SIMSTADT_TEMPLATE_PATH
environment variable. If unset, a Template/ directory in the current working directory
is used as a fallback.
"""

import logging
import os
import platform
import re
import shutil
import subprocess
from pathlib import Path
from xml.etree import ElementTree as et

from dotenv import load_dotenv

from .results import SimStadtResults, create_simstadt_results
from .utils import chdir, random_id

PARAMS = "params.xml"
SIMSTADT2_GLOB = "SimStadt2_0.*/"

load_dotenv()

log = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# SimStadt discovery
# ---------------------------------------------------------------------------

def find_simstadt(parent_folder: Path, simstadt_glob: str = SIMSTADT2_GLOB) -> Path:
    try:
        found = next(parent_folder.glob(simstadt_glob))
        log.info("# SimStadt found in %s\n", found)
        return found
    except StopIteration as e:
        raise ValueError(
            f"SimStadt not found in {parent_folder}/{simstadt_glob}. "
            "Set the SIMSTADT_FOLDER environment variable."
        ) from e


def get_simstadt_folder() -> Path:
    if "SIMSTADT_FOLDER" in os.environ:
        return Path(os.environ["SIMSTADT_FOLDER"])
    return find_simstadt(Path.home() / "Desktop")


def get_template_path() -> Path:
    if "SIMSTADT_TEMPLATE_PATH" in os.environ:
        return Path(os.environ["SIMSTADT_TEMPLATE_PATH"])
    fallback = Path.cwd() / "Template"
    if fallback.exists():
        return fallback
    raise ValueError(
        "No template path found. Set SIMSTADT_TEMPLATE_PATH or create a Template/ directory."
    )


# ---------------------------------------------------------------------------
# Low-level workflow helpers
# ---------------------------------------------------------------------------

def _simstadt_script() -> str:
    return "SimStadt.bat" if platform.system().lower() == "windows" else "./SimStadt.sh"


def _regionchooser_script() -> str:
    return "RegionChooser.bat" if platform.system().lower() == "windows" else "./RegionChooser.sh"


def check_paths(repo_path: Path, workflow_path: Path) -> None:
    if not repo_path.exists():
        raise ValueError(f"Repository not found: {repo_path}")
    if not workflow_path.exists():
        raise ValueError(f"Workflow not found: {workflow_path}")
    if not (workflow_path / PARAMS).exists():
        raise ValueError(f"No params.xml in {workflow_path}")


def prepare_workflow(workflow_path: Path, citygmls: list[str]) -> str:
    """Inject CityGML file names into the workflow's params.xml. Returns the workflow name."""
    tree = et.parse(workflow_path / PARAMS)
    root = tree.getroot()

    node = root.find(".//void[@property='name']/string")
    if node is None:
        raise ValueError("Unknown workflow!")
    name = node.text or "workflow"
    log.info("# Preparing '%s' workflow (%s):", name, workflow_path.name)

    citygml_node = root.find(".//void[@property='cityGmlFileNames']")
    if citygml_node is not None:
        for child in list(citygml_node):
            citygml_node.remove(child)
    else:
        workflow_node = root.find(".//object[@class='eu.simstadt.workflows.CityGmlWorkflow']")
        if workflow_node is None:
            raise ValueError("Broken workflow!")
        citygml_node = et.SubElement(workflow_node, "void")
        citygml_node.set("property", "cityGmlFileNames")

    for citygml in citygmls:
        log.info("  * Adding %s", citygml)
        add_node = et.SubElement(citygml_node, "void")
        add_node.set("method", "add")
        gml = et.SubElement(add_node, "string")
        gml.text = citygml

    tree.write(workflow_path / PARAMS)
    return name


def replace_params(workflow_path: Path, replaces: dict[str, str]) -> None:
    """Apply regex substitutions to all params.xml files under the workflow path."""
    param_files = list(workflow_path.glob(f"**/{PARAMS}"))
    replaced = False
    for param_file in param_files:
        with open(param_file, encoding="utf-8") as f:
            content = f.read()
        new_content = content
        for old, new in replaces.items():
            new_content = re.sub(old, new, new_content)
        if new_content != content:
            log.debug("Replacing parameters in %s", param_file)
            replaced = True
            with open(param_file, "w", encoding="utf-8") as f:
                f.write(new_content)
    if not replaced:
        log.warning("No parameter has been replaced! %r", replaces)


def copy_workflow_from_template(
    template_path: Path,
    project_path: Path,
    destination: str,
    replaces: dict | None = None,
) -> Path:
    template_path = template_path.with_suffix(".flow")
    workflow_path = (project_path / destination).with_suffix(".flow")
    shutil.copytree(template_path, workflow_path, dirs_exist_ok=True)
    if replaces:
        replace_params(workflow_path, replaces)
    return workflow_path


def _get_all_files(folder: Path) -> dict[Path, float]:
    return {f: f.stat().st_mtime for f in folder.glob("**/*") if f.is_file() and f.name != PARAMS}


def _compare_written_files(repo_path: Path, before: dict, after: dict) -> list[Path]:
    modified = []
    log.info("# Listing written files:")
    for result_file, mtime in after.items():
        if mtime > before.get(result_file, 0):
            log.info("  * '%s'", result_file.relative_to(repo_path))
            modified.append(result_file)
    log.info("")
    return modified


# ---------------------------------------------------------------------------
# Public execution API
# ---------------------------------------------------------------------------

def get_simstadt_output(path: Path) -> str:
    """Run SimStadt with the given path and return stdout regardless of exit code.

    Useful for extracting version information from the startup banner.
    """
    with chdir(get_simstadt_folder()):
        result = subprocess.run([_simstadt_script(), str(path)], text=True, capture_output=True, check=False)
        return result.stdout


def run_simstadt(workflow_path: Path, name: str) -> str:
    """Invoke the SimStadt CLI for the given workflow. Returns stdout."""
    with chdir(get_simstadt_folder()):
        log.info("Launching %s:", name)
        result = subprocess.run(
            [_simstadt_script(), str(workflow_path)], text=True, capture_output=True, check=False
        )
        if result.returncode != 0:
            log.warning("  Workflow failed!\n%s", result.stdout)
            raise ValueError("Workflow failed!")
        log.debug(result.stdout)
        log.info("  Workflow finished successfully!\n")
        return result.stdout


def run_regionchooser(*params: str) -> str:
    """Invoke the SimStadt RegionChooser CLI. Returns stdout."""
    with chdir(get_simstadt_folder()):
        result = subprocess.run(
            [_regionchooser_script(), *params], text=True, capture_output=True, check=True
        )
        log.debug(result.stdout)
        return result.stdout


def run_workflow(workflow_path: Path, citygmls: list[str]) -> list[Path]:
    """Prepare and run a SimStadt workflow. Returns the list of files written by SimStadt."""
    repo_path = workflow_path.parent.parent
    check_paths(repo_path, workflow_path)
    name = prepare_workflow(workflow_path, citygmls)
    before = _get_all_files(workflow_path)
    run_simstadt(workflow_path, name)
    after = _get_all_files(workflow_path)
    return _compare_written_files(repo_path, before, after)


def run_workflow_with_citygml(
    template: str | Path,
    citygml_path: Path,
    replaces: dict | None = None,
    description: str | None = None,
    destination: str | None = None,
    project_path: Path | None = None,
) -> SimStadtResults:
    """Copy a workflow template, inject parameters, run SimStadt, and return parsed results.

    Args:
        template: Template name (resolved via SIMSTADT_TEMPLATE_PATH) or full Path.
        citygml_path: Path to the CityGML input file.
        replaces: Optional dict of XML-fragment regex replacements for params.xml.
        description: Human-readable label for the result object.
        destination: Output workflow folder name; defaults to a timestamped random id.
        project_path: Directory where the workflow folder is created. Defaults to
            citygml_path.parent. When set to a different directory, citygml_path is
            passed as an absolute path so SimStadt can locate it.
    """
    if isinstance(template, str):
        template_path = get_template_path() / template
    else:
        template_path = Path(template)
    template_name = template_path.stem

    if destination is None:
        destination = random_id() + "_" + template_name.rsplit("_", 1)[-1]

    if project_path is None:
        project_path = citygml_path.parent
        citygml_ref = citygml_path.name
    else:
        repo_path = project_path.parent
        try:
            citygml_ref = str(citygml_path.relative_to(repo_path))
        except ValueError as e:
            raise ValueError(
                f"CityGML file {citygml_path} is not under the repository root {repo_path}. "
                "Place the GML file inside the repository directory or omit project_path."
            ) from e

    workflow_path = copy_workflow_from_template(template_path, project_path, destination, replaces)
    output_files = run_workflow(workflow_path, [citygml_ref])

    if description is None:
        description = f"{template_name} for {citygml_path.name}"
    return create_simstadt_results(description, output_files)
