"""
Abromics Data plugin for Madbot.

Provides access to Abromics raw input files via the Abromics HTTP API.
Downstream ENA workflows use madbot_ena_plugin; domain mapping may live in
abromics-analysis or in ``mapping.py`` for ENA field alignment.
"""

import logging
from pathlib import Path
from typing import Any, Dict, List, Optional
from urllib.parse import urlparse

import requests
from plugins_api import AccessType, ConnectionField, DataPlugin, PluginDataObject
from plugins_api.exceptions import DataNotFoundError, DownloadDataError

logger = logging.getLogger(__name__)


class AbromicsPlugin(DataPlugin):
    """
    Madbot Data plugin for Abromics file access.

    Resolves ``file_{id}`` objects against the Abromics API and supports
    download for submission pipelines (e.g. ENA via madbot_ena_plugin).
    """

    def __init__(self, connection_parameters: Dict[str, Any], **kwargs):
        """
        Initialize the Abromics Data plugin.

        Parameters
        ----------
        connection_parameters : dict[str, Any]
            Must contain 'abromics_url' and 'abromics_token'.
        """
        abromics_url_param = connection_parameters.get("abromics_url", "")
        if isinstance(abromics_url_param, dict):
            self.abromics_url = abromics_url_param.get("abromics_url", "").rstrip("/")
        else:
            self.abromics_url = str(abromics_url_param).rstrip("/") if abromics_url_param else ""

        abromics_token_param = connection_parameters.get("abromics_token", "")
        if isinstance(abromics_token_param, dict):
            self.abromics_token = abromics_token_param.get("abromics_token", "")
        else:
            self.abromics_token = str(abromics_token_param) if abromics_token_param else ""

        if not self.abromics_url or not self.abromics_token:
            raise ValueError("abromics_url and abromics_token are required")

    # ===== Plugin Identification =====

    @classmethod
    def get_name(cls) -> str:
        """Return the plugin name."""
        return "Abromics Data"

    @classmethod
    def get_slug(cls) -> str:
        """Return the plugin slug (stored on Connection rows)."""
        return "abromics_plugin"

    @classmethod
    def get_description(cls) -> str:
        """Return plugin description."""
        return (
            "Link Abromics genomic files in Madbot. "
            "Pairs with madbot_ena_plugin for ENA submission when configured."
        )

    @classmethod
    def get_connection_documentation_url(cls) -> str:
        """Return the URL for connection documentation."""
        return "https://www.ebi.ac.uk/ena/submit/webin/"

    @classmethod
    def get_author(cls) -> str:
        """Return the plugin author."""
        return "Abromics Team"

    @classmethod
    def get_logo(cls) -> str:
        """Return the logo path."""
        return "images/abromics_plugin.png"

    @classmethod
    def get_color(cls) -> str:
        """Return the plugin color."""
        return "#70BDBD"

    # ===== Connection Fields =====

    @classmethod
    def get_connection_fields(cls) -> tuple:
        """
        Return connection fields for Abromics API access.

        Madbot requires at least one shared param for connections.
        abromics_url is shared (workspace-level); abromics_token stays private.

        Returns
        -------
        tuple[list[ConnectionField], list[ConnectionField]]
            (shared_fields, private_fields)
        """
        _abromics_url_choices = [
            "http://localhost",
            "https://dev.analysis.abromics.fr",
            "https://analysis.abromics.fr",
        ]
        shared = [
            ConnectionField(
                slug="abromics_url",
                schema_content={
                    "type": "object",
                    "properties": {
                        "abromics_url": {
                            "title": "Abromics URL",
                            "description": "Abromics API base URL (no trailing slash).",
                            "type": "string",
                            "enum": _abromics_url_choices,
                            "default": _abromics_url_choices[0],
                        }
                    },
                    "required": ["abromics_url"],
                },
            ),
        ]
        private = [
            ConnectionField(
                slug="abromics_token",
                schema_content={
                    "type": "object",
                    "properties": {
                        "abromics_token": {
                            "description": "Abromics API token for file access",
                            "type": "string",
                            "access": "private",
                        }
                    },
                    "required": ["abromics_token"],
                },
            ),
        ]
        return shared, private

    def get_instance_name(self) -> str:
        """Return a display name for this connection instance."""
        parsed = urlparse(self.abromics_url)
        host = parsed.hostname or self.abromics_url
        return f"Abromics ({host})"

    def get_shared_parameters(self) -> Dict[str, str]:
        """Return shared parameters for the plugin."""
        return {"abromics_url": self.abromics_url}

    # ===== DataPlugin Methods =====

    @staticmethod
    def get_root_name() -> str:
        """Return the name of the root data object."""
        return "Abromics"

    @staticmethod
    def get_root_id() -> str:
        """Return the unique identifier of the root data object."""
        return "abromics_root"

    def _auth_headers(self) -> Dict[str, str]:
        return {"Authorization": f"Api-Key {self.abromics_token}"}

    def _get_json(self, path: str, *, params: Optional[dict] = None, timeout: int = 30) -> Any:
        # DRF pagination fix for `next` URLs (ex. "http://host/api/sample/?page=2").
        if isinstance(path, str) and path.startswith(("http://", "https://")):
            url = path
        else:
            url = f"{self.abromics_url.rstrip('/')}/{str(path).lstrip('/')}"
        try:
            resp = requests.get(url, headers=self._auth_headers(), params=params, timeout=timeout)
            resp.raise_for_status()
            return resp.json()
        except requests.exceptions.RequestException as e:
            logger.error("Abromics API GET %s failed: %s", url, e)
            raise

    @staticmethod
    def _format_sample_display(sample: Dict[str, Any]) -> tuple[str, str]:
        """
        Returns a (name, description) pair for a sample.

        We try to keep the list label compact while still exposing key identifiers
        (strain name + Abromics id) when present.
        """
        sid = sample.get("id")
        strain_id = sample.get("strain_id")
        abromics_id = sample.get("abromics_id")
        original_sample_id = sample.get("original_sample_id")
        perm = sample.get("permission")
        taxonomy = sample.get("taxonomy") or {}
        host = sample.get("host") or {}
        country = sample.get("country") or {}
        place = sample.get("place") or {}
        collected_date = sample.get("collected_date")

        sci = None
        if isinstance(taxonomy, dict):
            sci = taxonomy.get("scientific_name") or taxonomy.get("name")

        # Always show both strain id and original sample id in the label (when available),
        # plus Abromics id, so the explorer list is unambiguous.
        base = strain_id or original_sample_id or (f"sample_{sid}" if sid else "sample")
        orig_label = str(original_sample_id) if original_sample_id not in (None, "", "None") else "-"
        abro_label = str(abromics_id) if abromics_id not in (None, "", "None") else "-"

        # Build a display date (prefer collected_date; else from year/month/day).
        date_str: Optional[str] = None
        if collected_date:
            date_str = str(collected_date)
        else:
            y = sample.get("collected_year")
            m = sample.get("collected_month")
            d = sample.get("collected_day")
            date_bits: List[str] = []
            if y not in (None, "", "None"):
                date_bits.append(str(y))
            if m not in (None, "", "None"):
                date_bits.append(str(m).zfill(2))
            if d not in (None, "", "None"):
                date_bits.append(str(d).zfill(2))
            if date_bits:
                date_str = "-".join(date_bits)

        name_bits: List[str] = [f"{base} ({orig_label}) [{abro_label}]"]
        if sci:
            name_bits.append(str(sci))
        if date_str:
            name_bits.append(date_str)
        name = " — ".join(name_bits)

        def _is_present(v: Any) -> bool:
            if v is None:
                return False
            if isinstance(v, str) and (v.strip() == "" or v.strip() == "None"):
                return False
            return True

        desc_lines: List[str] = []

        def _add(label: str, value: Any) -> None:
            if not _is_present(value):
                return
            desc_lines.append(f"{label}: {value}")

        # Identifiers
        _add("Strain ID", strain_id)
        _add("Original sample ID", original_sample_id)
        _add("Abromics ID", abromics_id)

        # Core fields
        _add("Taxonomy", sci)

        host_species = host.get("species") if isinstance(host, dict) else None
        _add("Host species", host_species)

        _add("Accession number", sample.get("accession_number"))
        _add("Assembly method", sample.get("assembly_method"))
        _add("Submission date", sample.get("submission_date"))
        _add("Comment", sample.get("comments"))
        _add("Nucleic acid extraction", sample.get("nucleic_acid_extraction"))
        _add("Human DNA depletion", sample.get("human_dna_depletion"))

        country_name = country.get("name") if isinstance(country, dict) else None
        _add("Country", country_name)

        region = sample.get("country_level1") or {}
        region_name = region.get("name") if isinstance(region, dict) else None
        _add("Region", region_name)

        place_name = place.get("name") if isinstance(place, dict) else None
        _add("Place", place_name)

        _add("Collected", date_str)

        travel_countries = sample.get("travel_countries")
        if isinstance(travel_countries, list) and travel_countries:
            tc_names: List[str] = []
            for c in travel_countries:
                if isinstance(c, dict):
                    n = c.get("name")
                    if _is_present(n):
                        tc_names.append(str(n))
            if tc_names:
                _add("Travel countries", ", ".join(tc_names))

        sources = sample.get("sources")
        if isinstance(sources, list) and sources:
            src_bits: List[str] = []
            for s in sources:
                if not isinstance(s, dict):
                    continue
                st = s.get("type")
                sn = s.get("name")
                if _is_present(st) and _is_present(sn):
                    src_bits.append(f"{st}:{sn}")
                elif _is_present(sn):
                    src_bits.append(str(sn))
                elif _is_present(st):
                    src_bits.append(str(st))
            if src_bits:
                _add("Sources", ", ".join(src_bits))

        rif = sample.get("rawinputfiles")
        if isinstance(rif, list):
            _add("Raw input files", len(rif))

        _add("Permission", perm)
        _add("ID", sid)

        return name, ("\n".join(desc_lines) if desc_lines else "Abromics sample")

    def get_data_objects(self, parent_id: str) -> List[PluginDataObject]:
        """
        Return list of data objects from a given parent.

        Browsing model (Madbot UI):
        - root -> virtual folders (All samples / Public samples)
        - sample_<id> -> raw input files for the sample

        This lets users create datalinks directly from Madbot without knowing file ids.
        """
        root_id = self.get_root_id()
        if parent_id in ("", None, root_id):
            return [
                PluginDataObject(
                    id="abromics_samples_all",
                    name="My samples (private + public)",
                    type="folder",
                    icon="",
                    description="Samples you can access in Abromics (private + public), grouped by project.",
                    has_children=True,
                    linkable=False,
                ),
                PluginDataObject(
                    id="abromics_samples_public",
                    name="Samples (public)",
                    type="folder",
                    icon="",
                    description="Public samples in Abromics.",
                    has_children=True,
                    linkable=False,
                ),
            ]

        if parent_id == "abromics_samples_all":
            # Group accessible samples (private + public) by project.
            results: List[PluginDataObject] = []
            next_url: Optional[str] = "/api/project/"
            params = {"page_size": 200}
            while next_url:
                data = self._get_json(next_url, params=params if next_url == "/api/project/" else None)
                items = data.get("results", []) if isinstance(data, dict) else []
                for p in items:
                    pid = p.get("id")
                    if not pid:
                        continue
                    pname = p.get("name") or f"project_{pid}"
                    results.append(
                        PluginDataObject(
                            id=f"project_{pid}",
                            name=str(pname),
                            type="folder",
                            icon="",
                            description=f"Abromics project {pid}",
                            has_children=True,
                            linkable=False,
                        )
                    )
                next_url = data.get("next") if isinstance(data, dict) else None
                params = {}
            return results

        if parent_id == "abromics_samples_public":
            params = {"page_size": 200, "permission": "public"}

            results: List[PluginDataObject] = []
            next_url: Optional[str] = "/api/sample/"
            # Follow DRF pagination
            while next_url:
                data = self._get_json(next_url, params=params if next_url == "/api/sample/" else None)
                items = data.get("results", []) if isinstance(data, dict) else []
                for s in items:
                    sid = s.get("id")
                    if not sid:
                        continue
                    title, description = self._format_sample_display(s)
                    results.append(
                        PluginDataObject(
                            id=f"sample_{sid}",
                            name=str(title),
                            type="folder",
                            icon="",
                            description=description,
                            has_children=True,
                            linkable=False,
                        )
                    )
                next_url = data.get("next") if isinstance(data, dict) else None
                # Once we move to "next", params are already encoded in the URL
                params = {}
            return results

        if parent_id.startswith("project_"):
            project_id = parent_id.replace("project_", "", 1)
            # Accessible samples in this project (includes both private and public if applicable).
            # We do not filter by permission here to show the union.
            params = {"page_size": 200, "project_id": project_id}

            results: List[PluginDataObject] = []
            next_url: Optional[str] = "/api/sample/"
            while next_url:
                data = self._get_json(next_url, params=params if next_url == "/api/sample/" else None)
                items = data.get("results", []) if isinstance(data, dict) else []
                for s in items:
                    sid = s.get("id")
                    if not sid:
                        continue
                    title, description = self._format_sample_display(s)
                    results.append(
                        PluginDataObject(
                            id=f"sample_{sid}",
                            name=str(title),
                            type="folder",
                            icon="",
                            description=description,
                            has_children=True,
                            linkable=False,
                        )
                    )
                next_url = data.get("next") if isinstance(data, dict) else None
                params = {}
            return results

        if parent_id.startswith("sample_"):
            sample_id = parent_id.replace("sample_", "", 1)
            try:
                sample = self._get_json(f"/api/sample/{sample_id}/", timeout=30)
            except requests.exceptions.RequestException as e:
                raise DataNotFoundError(f"Sample {sample_id} not found") from e

            files = sample.get("rawinputfiles", []) if isinstance(sample, dict) else []
            out: List[PluginDataObject] = []
            for rf in files:
                rid = rf.get("id")
                if not rid:
                    continue
                filename = rf.get("original_name") or rf.get("filename") or f"file_{rid}"
                size = rf.get("size")
                ext = filename.split(".")[-1] if "." in filename else None
                obj = PluginDataObject(
                    id=f"file_{rid}",
                    name=str(filename),
                    type="file",
                    icon="",
                    description=f"Abromics file {rid}",
                    has_children=False,
                    linkable=True,
                    size=size,
                    file_extension=ext,
                )
                obj.add_external_access(
                    AccessType.URL,
                    f"{self.abromics_url}/api/download/inputfiles/{rid}",
                )
                out.append(obj)
            return out

        return []

    def get_data_object(self, object_id: str) -> PluginDataObject:
        """
        Return metadata for a data object (format: file_{file_id}).

        Fetches from Abromics GET /api/raw_input_file/{id}/
        """
        if object_id in (self.get_root_id(), "abromics_samples_all", "abromics_samples_public"):
            return PluginDataObject(
                id=object_id,
                name=self.get_root_name() if object_id == self.get_root_id() else object_id,
                type="folder",
                icon="",
                has_children=True,
                linkable=False,
            )
        if object_id.startswith("project_"):
            pid = object_id.replace("project_", "", 1)
            return PluginDataObject(
                id=object_id,
                name=f"Project {pid}",
                type="folder",
                icon="",
                description=f"Abromics project {pid}",
                has_children=True,
                linkable=False,
            )
        if object_id.startswith("sample_"):
            sid = object_id.replace("sample_", "", 1)
            try:
                sample = self._get_json(f"/api/sample/{sid}/", timeout=30)
            except requests.exceptions.RequestException as e:
                raise DataNotFoundError(f"Sample {sid} not found") from e
            title, description = self._format_sample_display(sample if isinstance(sample, dict) else {"id": sid})
            return PluginDataObject(
                id=object_id,
                name=str(title),
                type="folder",
                icon="",
                description=description,
                has_children=True,
                linkable=False,
            )

        if not object_id.startswith("file_"):
            raise DataNotFoundError(f"Unknown object type for ID: {object_id}")

        file_id = object_id.replace("file_", "")
        try:
            file_data = self._get_json(f"/api/raw_input_file/{file_id}/", timeout=30)
        except requests.exceptions.RequestException as e:
            logger.error("Error fetching file %s from Abromics: %s", file_id, e)
            raise DataNotFoundError(
                f"File {file_id} not found: could not fetch from Abromics API"
            ) from e

        filename = file_data.get("original_name") or f"file_{file_id}"
        file_size = file_data.get("size")
        ext = filename.split(".")[-1] if "." in filename else None
        obj = PluginDataObject(
            id=object_id,
            name=filename,
            type="file",
            icon="",
            description=f"Abromics file {file_id}",
            has_children=False,
            linkable=True,
            size=file_size,
            file_extension=ext,
        )
        obj.add_external_access(
            AccessType.URL,
            f"{self.abromics_url}/api/download/inputfiles/{file_id}",
        )
        return obj

    def download_data(self, external_dataobject_id: str, destination_path: str) -> str:
        """
        Download a data object to local storage.

        Parameters
        ----------
        external_dataobject_id : str
            Format: file_{file_id}
        destination_path : str
            Local path to save the file

        Returns
        -------
        str
            Path to the downloaded file
        """
        if not external_dataobject_id.startswith("file_"):
            raise ValueError(
                f"Invalid data object ID: {external_dataobject_id}. "
                "Expected 'file_{{file_id}}'"
            )
        file_id = external_dataobject_id.replace("file_", "")

        try:
            file_info = self.get_data_object(external_dataobject_id)
            download_url = None
            for acc in file_info.accesses:
                if acc.access_type == AccessType.URL:
                    download_url = acc.details
                    break
            if not download_url:
                raise DownloadDataError(f"File {file_id} has no URL access")

            response = requests.get(
                download_url,
                headers=self._auth_headers(),
                stream=True,
                timeout=300,
            )
            response.raise_for_status()

            dest = Path(destination_path)
            if dest.suffix == "" or (dest.exists() and dest.is_dir()):
                dest = dest / (file_info.name or f"file_{file_id}")
            dest.parent.mkdir(parents=True, exist_ok=True)

            with open(dest, "wb") as f:
                for chunk in response.iter_content(chunk_size=8192):
                    f.write(chunk)

            logger.info("Downloaded %s to %s", external_dataobject_id, dest)
            return str(dest)

        except requests.exceptions.RequestException as e:
            logger.error("Error downloading file %s: %s", file_id, e)
            raise DownloadDataError(f"Failed to download file {file_id}") from e

    @classmethod
    def get_supported_file_extensions(cls) -> List[str]:
        """Return supported file extensions."""
        return ["fastq", "fq", "fastq.gz", "fq.gz", "fasta", "fasta.gz", "fa", "fa.gz", "gz"]
