"""
Querying the Placco et al. (2014) carbon-correction online tool.

The online calculator at https://vplacco.pythonanywhere.com/ requires: log(g), [Fe/H], and [C/Fe].
Then, it returns the evolutionary correction to the observed [C/Fe] abundance.

Parameter space (values outside are clipped to the nearest grid point):
    log(g)  :   0.0  to  5.0   (step 0.1)
    [Fe/H]  :  -5.5  to  0.0   (step 0.1)
    [C/Fe]  :  -1.0  to  3.0   (step 0.1)
"""

from __future__ import annotations

import re
import time
from typing import Optional

import numpy as np
import pandas as pd
import requests
from bs4 import BeautifulSoup

# ── configuration ────────────────────────────────────────────────────────────
BASE_URL: str = "https://vplacco.pythonanywhere.com"
DEFAULT_DELAY: float = 0.1
DEFAULT_TIMEOUT: int = 30

# Module-level cache, populated on first use by _ensure_form_discovered()
_FORM_ACTION: Optional[str] = None
_FORM_METHOD: Optional[str] = None
_FORM_FIELDS: Optional[dict] = None


# ── form discovery ───────────────────────────────────────────────────────────

def discover_form(url: str = BASE_URL, verbose: bool = True) -> dict:
    """Fetch the website and inspect the HTML ``<form>`` element.

    Returns
    -------
    dict
        ``action``     – the form's target URL path
        ``method``     – ``'POST'`` or ``'GET'``
        ``fields``     – mapping ``{'logg': <name>, 'feh': <name>, 'cfe': <name>}``
        ``raw_inputs`` – list of all discovered ``<input>`` name attributes
    """
    resp = requests.get(url, timeout=DEFAULT_TIMEOUT)
    resp.raise_for_status()
    soup = BeautifulSoup(resp.text, "html.parser")

    forms = soup.find_all("form")
    if not forms:
        raise RuntimeError("No <form> found on the page.")

    best_form = None
    for i, form in enumerate(forms):
        action = form.get("action", "/")
        method = form.get("method", "GET").upper()
        inputs = []
        for inp in form.find_all(["input", "select"]):
            name = inp.get("name")
            itype = inp.get("type", "text")
            if name and itype not in ("submit", "hidden", "button"):
                inputs.append(name)
        if verbose:
            print(
                f"[form {i}] action={action!r}  method={method}  "
                f"inputs={inputs}"
            )
        if len(inputs) >= 3:
            best_form = {"action": action, "method": method, "inputs": inputs}

    if best_form is None and forms:
        form = forms[0]
        action = form.get("action", "/")
        method = form.get("method", "GET").upper()
        inputs = [
            inp.get("name")
            for inp in form.find_all(["input", "select"])
            if inp.get("name")
            and inp.get("type", "text") not in ("submit", "hidden", "button")
        ]
        best_form = {"action": action, "method": method, "inputs": inputs}

    if best_form is None:
        raise RuntimeError("No suitable form found on the page.")

    field_map = _map_field_names(best_form["inputs"])
    info = {
        "action": best_form["action"],
        "method": best_form["method"],
        "fields": field_map,
        "raw_inputs": best_form["inputs"],
    }
    if verbose:
        print(f"\nBest form:")
        print(f"  Action : {info['action']}")
        print(f"  Method : {info['method']}")
        print(f"  Fields : {info['fields']}")
    return info


def _map_field_names(input_names: list[str]) -> dict:
    """Map HTML input names to canonical roles ``logg``, ``feh``, ``cfe``."""
    mapping: dict[str, str] = {}
    for name in input_names:
        low = name.lower().replace("_", "").replace("-", "").replace(" ", "")
        if "logg" in low or "log_g" in name.lower() or "gravity" in low:
            mapping["logg"] = name
        elif "feh" in low or "fe/h" in low or "metallicity" in low:
            mapping["feh"] = name
        elif "cfe" in low or "c/fe" in low or "carbon" in low:
            mapping["cfe"] = name

    if len(mapping) < 3 and len(input_names) >= 3:
        if "logg" not in mapping:
            mapping["logg"] = input_names[0]
        if "feh" not in mapping:
            mapping["feh"] = input_names[1]
        if "cfe" not in mapping:
            mapping["cfe"] = input_names[2]

    return mapping


def _ensure_form_discovered() -> None:
    """Populate the module-level form cache on first call."""
    global _FORM_ACTION, _FORM_METHOD, _FORM_FIELDS
    if _FORM_FIELDS is not None:
        return
    try:
        info = discover_form(verbose=False)
        _FORM_ACTION = info["action"]
        _FORM_METHOD = info["method"]
        _FORM_FIELDS = info["fields"]
    except Exception as exc:
        raise RuntimeError(
            f"Could not auto-discover form on {BASE_URL}: {exc}\n"
            f"Run  carbcor.discover_form(verbose=True)  for details."
        ) from exc


def reset_form_cache() -> None:
    """Clear the cached form metadata so the next call re-discovers it.

    Useful if the remote website has been updated.
    """
    global _FORM_ACTION, _FORM_METHOD, _FORM_FIELDS
    _FORM_ACTION = None
    _FORM_METHOD = None
    _FORM_FIELDS = None


# ── single-star query ────────────────────────────────────────────────────────

def get_carbon_correction(
    logg: float,
    feh: float,
    cfe: float,
    *,
    timeout: int = DEFAULT_TIMEOUT,
) -> dict:
    """Query the web tool for one star and return the corrected [C/Fe].

    Parameters
    ----------
    logg : float
        Surface gravity log(g).
    feh : float
        Iron abundance [Fe/H].
    cfe : float
        Carbon-to-iron ratio [C/Fe].
    timeout : int, optional
        HTTP request timeout in seconds (default 30).

    Returns
    -------
    dict
        ``logg_input``     – echo of the input log(g)
        ``feh_input``      – echo of the input [Fe/H]
        ``cfe_input``      – echo of the input [C/Fe]
        ``cfe_corrected``  – corrected [C/Fe]  (``NaN`` on failure)
        ``correction``     – applied correction delta (``NaN`` on failure)
        ``status``         – ``'success'`` or ``'error'``
        ``message``        – error detail string (empty on success)
    """
    out: dict = dict(
        logg_input=logg,
        feh_input=feh,
        cfe_input=cfe,
        cfe_corrected=float("nan"),
        correction=float("nan"),
        status="error",
        message="",
    )

    _ensure_form_discovered()

    payload = {
        _FORM_FIELDS["logg"]: str(logg),
        _FORM_FIELDS["feh"]: str(feh),
        _FORM_FIELDS["cfe"]: str(cfe),
    }

    action = _FORM_ACTION or "/"
    if action.startswith("http"):
        target_url = action
    else:
        target_url = BASE_URL.rstrip("/") + "/" + action.lstrip("/")

    method = (_FORM_METHOD or "POST").upper()

    try:
        if method == "POST":
            resp = requests.post(target_url, data=payload, timeout=timeout)
        else:
            resp = requests.get(target_url, params=payload, timeout=timeout)
        resp.raise_for_status()
    except requests.exceptions.ConnectionError:
        out["message"] = (
            "Connection failed. Check internet and that the site is up."
        )
        return out
    except requests.exceptions.Timeout:
        out["message"] = f"Timed out after {timeout}s."
        return out
    except requests.exceptions.HTTPError as exc:
        out["message"] = f"HTTP error: {exc}"
        return out

    # ── parse the response ────────────────────────────────────────────────
    soup = BeautifulSoup(resp.text, "html.parser")
    text = soup.get_text(" ", strip=True)
    val = _extract_corrected(soup, text, cfe)

    if val is not None:
        out["cfe_corrected"] = round(val + cfe, 4)
        out["correction"] = round(val, 4)
        out["status"] = "success"
    else:
        out["message"] = (
            "Could not parse corrected [C/Fe] from the response. "
            "Run  carbcor.debug_response()  to see what the server returns."
        )
        out["_raw_html"] = resp.text

    return out


# ── debug helper ─────────────────────────────────────────────────────────────

def debug_response(
    logg: float = 2.0,
    feh: float = -3.0,
    cfe: float = 1.5,
) -> "requests.Response":
    """Send a test query and print the full server response for debugging.

    Returns the raw :class:`requests.Response` object.
    """
    _ensure_form_discovered()
    print(f"Form config:")
    print(f"  action = {_FORM_ACTION}")
    print(f"  method = {_FORM_METHOD}")
    print(f"  fields = {_FORM_FIELDS}")
    print()

    payload = {
        _FORM_FIELDS["logg"]: str(logg),
        _FORM_FIELDS["feh"]: str(feh),
        _FORM_FIELDS["cfe"]: str(cfe),
    }
    action = _FORM_ACTION or "/"
    if action.startswith("http"):
        target_url = action
    else:
        target_url = BASE_URL.rstrip("/") + "/" + action.lstrip("/")

    print(f"{_FORM_METHOD} {target_url}")
    print(f"  payload = {payload}")
    print()

    if (_FORM_METHOD or "POST").upper() == "POST":
        resp = requests.post(target_url, data=payload, timeout=DEFAULT_TIMEOUT)
    else:
        resp = requests.get(target_url, params=payload, timeout=DEFAULT_TIMEOUT)

    print(f"Status: {resp.status_code}")
    print(f"Content-Type: {resp.headers.get('content-type')}")
    print(f"Final URL: {resp.url}")
    print()

    soup = BeautifulSoup(resp.text, "html.parser")
    print("--- visible page text ---")
    print(soup.get_text("\n", strip=True)[:3000])
    print("--- end ---")

    return resp


# ── HTML parsing ─────────────────────────────────────────────────────────────
_NUM = r"[-+]?\d+\.?\d*"

_PATTERNS = [
    rf"\[C/Fe\]\s*(?:corrected|corr\.?)\s*[=:]\s*({_NUM})",
    rf"corrected\s*\[C/Fe\]\s*[=:]\s*({_NUM})",
    rf"[Cc]orrection\s*[=:]\s*({_NUM})",
    rf"result\s*[=:]\s*({_NUM})",
    rf"corrected\s*[=:]\s*({_NUM})",
    rf"\[C/Fe\]\s*=\s*({_NUM})",
]


def _extract_corrected(
    soup: BeautifulSoup,
    text: str,
    cfe_input: float,
) -> Optional[float]:
    """Extract the correction delta from the HTML response."""

    for pat in _PATTERNS:
        m = re.search(pat, text, re.IGNORECASE)
        if m:
            v = float(m.group(1))
            if v >= cfe_input - 0.01 and abs(v - cfe_input) < 5.0:
                return v

    for tag in soup.find_all(
        ["b", "strong", "span", "td", "th", "p", "div",
         "h1", "h2", "h3", "h4"]
    ):
        t = tag.get_text(strip=True)
        m = re.fullmatch(rf"({_NUM})", t)
        if m:
            v = float(m.group(1))
            if v >= cfe_input - 0.01 and abs(v - cfe_input) < 5.0:
                return v

    for kw in ("corrected", "correction", "result", "output", "C/Fe"):
        idx = text.lower().find(kw.lower())
        if idx < 0:
            continue
        m = re.search(rf"({_NUM})", text[idx:])
        if m:
            v = float(m.group(1))
            if -2.0 <= v <= 6.0:
                return v

    all_floats = re.findall(r"([-+]?\d+\.\d+)", text)
    input_strs = {
        f"{cfe_input:.1f}", f"{cfe_input:.2f}",
        f"{cfe_input:.4f}", str(cfe_input),
    }
    candidates = []
    for fs in all_floats:
        if fs in input_strs:
            continue
        v = float(fs)
        if v >= cfe_input and abs(v - cfe_input) < 3.0:
            candidates.append(v)
    if len(candidates) == 1:
        return candidates[0]

    return None


# ── batch DataFrame function ─────────────────────────────────────────────────

def apply_carbon_corrections(
    df: pd.DataFrame,
    logg_col: str = "logg",
    feh_col: str = "feh",
    cfe_col: str = "cfe",
    *,
    delay: float = DEFAULT_DELAY,
    timeout: int = DEFAULT_TIMEOUT,
    verbose: bool = True,
) -> pd.DataFrame:
    """Apply Placco et al. (2014) carbon corrections to a DataFrame.

    Parameters
    ----------
    df : pandas.DataFrame
        Must contain columns for log(g), [Fe/H], and [C/Fe].
    logg_col : str
        Column name for log(g) values (default ``'logg'``).
    feh_col : str
        Column name for [Fe/H] values (default ``'feh'``).
    cfe_col : str
        Column name for [C/Fe] values (default ``'cfe'``).
    delay : float
        Seconds to wait between web requests (default 0.5).
    timeout : int
        Per-request HTTP timeout in seconds (default 30).
    verbose : bool
        Print progress to stdout (default ``True``).

    Returns
    -------
    pandas.DataFrame
        A copy of *df* with four new columns:

        - ``cfe_corrected`` – corrected [C/Fe] (``NaN`` if failed)
        - ``correction``    – correction delta (``NaN`` if failed)
        - ``corr_status``   – ``'success'`` or ``'error'``
        - ``corr_message``  – error details (empty on success)
    """
    for col, label in [
        (logg_col, "log(g)"),
        (feh_col, "[Fe/H]"),
        (cfe_col, "[C/Fe]"),
    ]:
        if col not in df.columns:
            raise ValueError(
                f"Column '{col}' ({label}) not in DataFrame. "
                f"Available: {list(df.columns)}"
            )

    res = df.copy()
    n = len(df)
    res["cfe_corrected"] = np.nan
    res["correction"] = np.nan
    res["corr_status"] = ""
    res["corr_message"] = ""

    if verbose:
        print(f"Calculating carbon corrections for {n} star(s) ...")
        print(f"  Tool : {BASE_URL}")
        print(f"  Ref  : Placco et al. (2014, ApJ, 797, 21)")
        print("-" * 62)

    ok = err = 0
    for i, (idx, row) in enumerate(df.iterrows()):
        lg = row[logg_col]
        fe = row[feh_col]
        cf = row[cfe_col]

        if pd.isna(lg) or pd.isna(fe) or pd.isna(cf):
            res.at[idx, "corr_status"] = "error"
            res.at[idx, "corr_message"] = "NaN in input"
            err += 1
            if verbose:
                print(f"  [{i+1}/{n}] skipped (NaN)")
            continue

        if verbose:
            print(
                f"  [{i+1}/{n}] log(g)={lg: .2f}  "
                f"[Fe/H]={fe: .2f}  [C/Fe]={cf: .2f}  ->  ",
                end="", flush=True,
            )

        c = get_carbon_correction(lg, fe, cf, timeout=timeout)

        res.at[idx, "cfe_corrected"] = c["cfe_corrected"]
        res.at[idx, "correction"] = c["correction"]
        res.at[idx, "corr_status"] = c["status"]
        res.at[idx, "corr_message"] = c["message"]

        if c["status"] == "success":
            ok += 1
            if verbose:
                print(
                    f"[C/Fe]_corr = {c['cfe_corrected']:+.2f}  "
                    f"(delta = +{c['correction']:.2f})"
                )
        else:
            err += 1
            if verbose:
                print(f"ERROR - {c['message'][:80]}")

        if i < n - 1:
            time.sleep(delay)

    if verbose:
        print("-" * 62)
        print(f"Done:  {ok} succeeded,  {err} failed  ({n} total)")

    return res
