#pyright: reportPrivateImportUsage=false
from __future__ import annotations

import re
from typing import Any, Protocol, TYPE_CHECKING

import click
from rich.console import Console

if TYPE_CHECKING:
    from relationalai.config.config import Config


class _ReasonersSizing(Protocol):
    def sizes(self) -> list[str]: ...
    def validate_size(self, size: str | None) -> tuple[bool, list[str]]: ...
    def list(
        self,
        *,
        state: str | None = None,
        reasoner_name: str | None = None,
        reasoner_type: str | None = None,
        reasoner_size: str | None = None,
        created_by: str | None = None,
    ) -> list[Any]: ...
    def delete(self, reasoner_type: str, reasoner_name: str | None = None) -> None: ...
    def suspend(self, reasoner_type: str, reasoner_name: str | None = None) -> None: ...
    def resume_ready(self, reasoner_type: str, reasoner_name: str | None = None, *, timeout_s: int = 900) -> Any: ...


class _JobsOps(Protocol):
    def list(
        self,
        reasoner_type: str | None = None,
        *,
        job_id: str | None = None,
        state: str | None = None,
        name: str | None = None,
        created_by: str | None = None,
        only_active: bool = False,
        all_users: bool = False,
        limit: int | None = None,
        include: Any | None = None,
    ) -> list[Any]: ...

    def get(self, reasoner_type: str, job_id: str, *, include: Any | None = None) -> Any | None: ...


_REASONER_NAME_REGEX = re.compile(r"^[a-zA-Z]\w{2,}$")
_REASONER_NAME_ERROR = "Min 3 chars, start with letter, only letters, numbers, underscores allowed."


def validate_reasoner_name(name: str) -> tuple[bool, str | None]:
    if not isinstance(name, str) or not _REASONER_NAME_REGEX.match(name):
        return False, _REASONER_NAME_ERROR
    return True, None


def ensure_reasoner_type(
    reasoner_type: str | None,
    *,
    interactive: bool,
    console: Console,
) -> tuple[str, str]:
    """Return `(t_norm, t_label)` for a reasoner type, prompting if needed."""
    from relationalai.services.reasoners.models import ReasonerType
    from relationalai.tools.cli.components import SelectOption, fuzzy_select

    allowed = [ReasonerType.LOGIC, ReasonerType.PRESCRIPTIVE, ReasonerType.PREDICTIVE]
    raw = (reasoner_type or "").strip()

    # Accept the user-facing type names (Logic/Prescriptive/Predictive) case-insensitively.
    label_to_type = {ReasonerType.get_label(t).strip().upper(): t for t in allowed}
    candidate = str(reasoner_type or "").strip().upper() if raw else ""
    if candidate and candidate in label_to_type:
        reasoner_type = label_to_type[candidate]
        candidate = str(reasoner_type).strip().upper()

    if candidate not in allowed:
        if not interactive:
            valid = ", ".join(ReasonerType.get_label(t) for t in allowed)
            if not raw:
                raise click.ClickException(
                    "Reasoner type is required. "
                    f"Valid types: {valid}"
                )
            raise click.ClickException(
                f"Invalid reasoner type {raw!r}. "
                f"Valid types: {valid}"
            )

        options = [
            SelectOption(value=t, label=f"{ReasonerType.get_label(t)}: {ReasonerType.get_description(t)}") for t in allowed
        ]
        reasoner_type = str(fuzzy_select("Reasoner type:", options, default=allowed[0]))
        console.print()

    t_norm = ReasonerType.normalize(reasoner_type)
    return t_norm, ReasonerType.get_label(t_norm)


def ensure_reasoner_name(
    reasoner_name: str | None,
    *,
    interactive: bool,
    console: Console,
) -> str:
    """Return a validated reasoner name, prompting if needed."""
    from relationalai.tools.cli.components import text_prompt

    raw = (reasoner_name or "").strip()

    if interactive and not raw:
        console.print("Note: [cyan]Reasoner names are case sensitive[/cyan]")
        console.print()

    if not raw:
        if not interactive:
            raise click.ClickException("Reasoner name is required.")
        out = text_prompt(
            "Reasoner name:",
            default=None,
            validator=_REASONER_NAME_REGEX.match,  # type: ignore[arg-type]
            invalid_message=_REASONER_NAME_ERROR,
        )
        console.print()
        return out

    ok, msg = validate_reasoner_name(raw)
    if ok:
        return raw

    if not interactive:
        raise click.ClickException(f"Invalid reasoner name {raw!r}. {msg}")

    console.print(f"[yellow]{msg}[/yellow]")
    out = text_prompt(
        "Reasoner name:",
        default=None,
        validator=_REASONER_NAME_REGEX.match,  # type: ignore[arg-type]
        invalid_message=_REASONER_NAME_ERROR,
    )
    console.print()
    return out


def ensure_reasoner_size(
    reasoners: _ReasonersSizing,
    reasoner_size: str | None,
    *,
    interactive: bool,
    console: Console,
) -> str | None:
    """Return a validated size (or None for default), prompting if needed."""
    from relationalai.tools.cli.components import SelectOption, fuzzy_select

    raw = (reasoner_size or "").strip()

    # Explicit size: validate and (if interactive) offer a picker on failure.
    if raw:
        ok, allowed = reasoners.validate_size(raw)
        if ok:
            return raw

        if not interactive:
            raise click.ClickException(f"Invalid reasoner size {raw!r}. Allowed sizes: {', '.join(allowed)}")

        options = [SelectOption(value=s, label=s) for s in allowed]
        picked = fuzzy_select("Reasoner size:", options, default=allowed[0] if allowed else None)
        console.print()
        return str(picked) if picked is not None else None

    # Missing size: in interactive flows, ask (and allow "default from config").
    if interactive:
        allowed = reasoners.sizes()
        options: list[SelectOption] = [SelectOption(value=None, label="default (from config)")]
        options.extend(SelectOption(value=s, label=s) for s in allowed)
        picked = fuzzy_select("Reasoner size:", options, default=None)
        console.print()
        return str(picked) if picked is not None else None

    # Non-interactive: let backend/config defaults apply.
    return None


def ensure_job_type(
    job_type: str | None,
    *,
    interactive: bool,
    console: Console,
) -> tuple[str, str]:
    """Return `(t_public, t_label)` for a jobs/transactions type.

    The Jobs service uses a `type` discriminator that is *not* always the same as
    reasoner types:

    - `LOGIC` maps to transactions
    - `SOLVER` maps to prescriptive jobs
    - `ML` maps to predictive jobs

    For CLI ergonomics we accept only the user-facing labels:
    - Logic / Prescriptive / Predictive

    Note: the underlying Jobs service uses discriminators like `SOLVER`/`ML`, but
    those are considered internal and are intentionally not accepted by the CLI.
    """
    from relationalai.tools.cli.components import SelectOption, fuzzy_select

    from relationalai.tools.cli.job_types import (
        JOB_TYPES,
        JOB_TYPE_LOGIC,
        JOB_TYPE_ML,
        JOB_TYPE_SOLVER,
        normalize_job_type,
        job_type_label,
    )

    raw = (job_type or "").strip()
    candidate = ""
    if raw:
        # Enforce public-only types in the CLI boundary.
        # Reject the internal Jobs discriminators when provided as-is.
        # (Allow case-insensitive public labels like 'logic'.)
        if raw == raw.upper() and raw in {JOB_TYPE_LOGIC, JOB_TYPE_SOLVER, JOB_TYPE_ML}:
            raise click.ClickException(
                f"Invalid job type {raw!r}. Expected: Logic, Prescriptive, or Predictive."
            )
        try:
            candidate = normalize_job_type(raw)
        except Exception:
            candidate = ""

    if candidate not in JOB_TYPES:
        if not interactive:
            valid = ", ".join(["Logic", "Prescriptive", "Predictive"])
            if not raw:
                raise click.ClickException(f"Job type is required. Valid types: {valid}")
            raise click.ClickException(f"Invalid job type {raw!r}. Valid types: {valid}")

        from relationalai.services.reasoners.models import ReasonerType

        options = [
            SelectOption(
                value=JOB_TYPE_LOGIC,
                label=f"{ReasonerType.get_label(ReasonerType.LOGIC)}: {ReasonerType.get_description(ReasonerType.LOGIC)}",
            ),
            SelectOption(
                value=JOB_TYPE_SOLVER,
                label=f"{ReasonerType.get_label(ReasonerType.PRESCRIPTIVE)}: {ReasonerType.get_description(ReasonerType.PRESCRIPTIVE)}",
            ),
            SelectOption(
                value=JOB_TYPE_ML,
                label=f"{ReasonerType.get_label(ReasonerType.PREDICTIVE)}: {ReasonerType.get_description(ReasonerType.PREDICTIVE)}",
            ),
        ]
        candidate = str(fuzzy_select("Job reasoner type:", options, default=JOB_TYPE_LOGIC))
        console.print()

    # `candidate` is a canonical Jobs discriminator (LOGIC/SOLVER/ML); return the public label.
    label = job_type_label(candidate) or candidate
    return label, label


def get_current_username(config: "Config") -> str | None:
    """Best-effort current username (used for created_by filtering)."""
    try:
        conn = config.get_default_connection()
        user = getattr(conn, "user", None)
        if isinstance(user, str) and user.strip():
            return user.strip()
    except Exception:
        return None
    return None


def select_reasoner(
    reasoners: _ReasonersSizing,
    *,
    reasoner_type: str | None,
    reasoner_name: str | None,
    state: str | None = None,
    exclude_states: set[str] | None = None,
    interactive: bool,
    console: Console,
    prompt: str,
) -> tuple[str, str] | None:
    """Select a reasoner (type,name).

    - If `reasoner_type` is provided, list is filtered by type
    - If `reasoner_name` is provided, list is filtered by exact name (case-insensitive)
    - If `state` is provided, list is filtered by that state (backend-side)
    - If `exclude_states` is provided, list excludes those states (client-side)
    """
    from relationalai.services.reasoners.models import ReasonerType
    from relationalai.tools.cli.components import SelectOption, fuzzy_select

    # Validate + normalize type filter (optional).
    allowed = [ReasonerType.LOGIC, ReasonerType.PRESCRIPTIVE, ReasonerType.PREDICTIVE]
    t_filter: str | None = None
    raw_type = (reasoner_type or "").strip()
    if raw_type:
        t_filter, _t_label = ensure_reasoner_type(raw_type, interactive=interactive, console=console)

    # Keep selection behavior consistent with `rai reasoners:list`:
    # list all reasoners unless the user explicitly requests narrower filters.
    name_filter = (reasoner_name or "").strip()
    items = reasoners.list(
        created_by=None,
        reasoner_type=t_filter,
        state=state,
        reasoner_name=name_filter or None,
    )

    if name_filter:
        items = [r for r in items if getattr(r, "name", "").upper() == name_filter.upper()]
    if exclude_states:
        banned = {str(s).upper() for s in exclude_states}
        items = [r for r in items if str(getattr(r, "state", "") or "").upper() not in banned]

    if not items:
        console.print("[yellow]No reasoners found.[/yellow]" if interactive else "No reasoners found.")
        return None

    # If a specific name resolves uniquely, skip the interactive picker.
    if name_filter and len(items) == 1:
        r0 = items[0]
        return str(getattr(r0, "type", "")), str(getattr(r0, "name", ""))

    def type_label(t: str) -> str:
        t_norm = ReasonerType.normalize(t)
        return ReasonerType.get_label(t_norm) if t_norm in allowed else t_norm

    options: list[SelectOption] = []
    for r in items:
        r_name = getattr(r, "name", "")
        r_type = getattr(r, "type", "")
        r_size = getattr(r, "size", "") or ""
        label = f"{r_name}, {type_label(str(r_type))}, {r_size}".rstrip(", ").rstrip()
        options.append(SelectOption(value=(str(r_type), str(r_name)), label=label))

    picked = fuzzy_select(prompt, options, default=options[0].value)
    console.print()
    if not isinstance(picked, tuple) or len(picked) != 2:
        raise click.ClickException("Invalid selection.")
    t, n = picked
    return str(t), str(n)


def select_job(
    jobs: _JobsOps,
    *,
    config: "Config",
    reasoner_type: str | None,
    job_id: str | None,
    interactive: bool,
    console: Console,
    prompt: str,
    only_active: bool = False,
    state: str | None = None,
    name: str | None = None,
) -> tuple[str, str] | None:
    """Select a job (reasoner_type, job_id) from the current user's jobs.

    - Uses `created_by=<current_user>` when available.
    - If `reasoner_type` provided, validates it and filters the list.
    - If `job_id` provided, filters to that job id.
    """
    from relationalai.tools.cli.components import SelectOption, fuzzy_select

    t_filter: str | None = None
    if reasoner_type is not None and str(reasoner_type).strip():
        t_filter, _t_label = ensure_job_type(reasoner_type, interactive=interactive, console=console)

    created_by = get_current_username(config)
    jid = (job_id or "").strip() or None

    items = jobs.list(
        t_filter,
        job_id=jid,
        state=state,
        name=name,
        created_by=created_by,
        only_active=bool(only_active),
        all_users=False,
        limit=None,
        include=None,
    )

    if not items:
        console.print("[yellow]No jobs found.[/yellow]" if interactive else "No jobs found.")
        return None

    from relationalai.tools.cli.job_types import job_type_label

    def type_label(t: str) -> str:
        return job_type_label(t) or (str(t or "").strip().upper())

    options: list[SelectOption] = []
    for j in items:
        jid2 = str(getattr(j, "id", "") or "")
        rt = str(getattr(j, "reasoner_type", "") or "")
        st = str(getattr(j, "state", "") or "")
        nm = str(getattr(j, "name", "") or "")
        rt_label = type_label(rt)
        label = f"{jid2}, {rt_label}, {st}, {nm}".rstrip(", ").rstrip()
        # Return the public job type label to keep the CLI surface stable.
        options.append(SelectOption(value=(rt_label, jid2), label=label))

    picked = fuzzy_select(prompt, options, default=options[0].value)
    console.print()
    if not isinstance(picked, tuple) or len(picked) != 2:
        raise click.ClickException("Invalid selection.")
    t, jid_out = picked
    return str(t), str(jid_out)

