"""DataTools Automated Workflows — Streamlit page."""

from __future__ import annotations

import io
import json
import sys
from pathlib import Path

import pandas as pd
import streamlit as st

_project_root = Path(__file__).resolve().parent.parent.parent.parent
if str(_project_root) not in sys.path:
    sys.path.insert(0, str(_project_root))

from src.gui.components import (
    back_to_home_link,
    render_sticky_footer,
    render_tool_header,
    hide_streamlit_chrome,
    html_download_button,
    pickup_or_upload,
    require_feature_or_render_upgrade,
)
from src.core.pipeline import (
    Pipeline,
    SOFT_DEPENDENCIES,
    Step,
    TOOL_NAMES,
    recommended_pipeline,
    run_pipeline,
    validate_pipeline,
)
from src.gui.components.pipeline_modules import (
    render_step_card,
    step_label,
    step_phrase,
    step_status,
)
from src.license import FeatureFlag

hide_streamlit_chrome()
render_sticky_footer()
back_to_home_link()
from src.audit import log_page_open
log_page_open("9_Pipeline_Runner")
require_feature_or_render_upgrade(FeatureFlag.PIPELINE_RUNNER)


# ---------------------------------------------------------------------------
# Header
# ---------------------------------------------------------------------------

render_tool_header("09_pipeline_runner")


# ---------------------------------------------------------------------------
# File upload
# ---------------------------------------------------------------------------

uploaded = pickup_or_upload(
    label="Import CSV or Excel file",
    key="pipeline_file_upload",
    types=["csv", "tsv", "xlsx", "xls"],
)

if uploaded is None:
    st.info("Import a CSV, TSV, or Excel file to begin.")
    st.stop()


@st.cache_data(show_spinner=False)
def _read_uploaded(name: str, data: bytes) -> pd.DataFrame:
    suffix = Path(name).suffix.lower()
    bio = io.BytesIO(data)
    if suffix in (".xlsx", ".xls"):
        return pd.read_excel(bio)
    for enc in ("utf-8", "utf-8-sig", "latin-1"):
        try:
            bio.seek(0)
            sep = "\t" if suffix == ".tsv" else ","
            return pd.read_csv(bio, encoding=enc, sep=sep, on_bad_lines="warn")
        except UnicodeDecodeError:
            continue
    bio.seek(0)
    return pd.read_csv(bio, encoding="latin-1")


try:
    df = _read_uploaded(uploaded.name, uploaded.getvalue())
except Exception as e:
    from src.core.errors import format_for_user
    st.error(
        f"**Could not read `{uploaded.name}`**\n\n"
        f"```\n{format_for_user(e)}\n```"
    )
    st.stop()

# Collapse the input preview and pipeline editor once the user has clicked
# Run Pipeline so the Results section below is the primary visual focus.
# The user can re-expand either expander to re-inspect or adjust.
_has_result = st.session_state.get("pipeline_result") is not None

with st.expander(f"Preview: {uploaded.name}", expanded=not _has_result):
    st.caption(f"{len(df)} rows, {len(df.columns)} columns")
    st.dataframe(df.head(10), width="stretch")

st.divider()


# ---------------------------------------------------------------------------
# Pipeline builder — visual module cards
# ---------------------------------------------------------------------------
#
# Each step is a "module" card (src/gui/components/pipeline_modules.py) with a
# plain-language Configure panel — no raw JSON. Steps live in session state as
# an ordered list of dicts, each carrying a STABLE integer id so widget keys
# survive reorder/remove. Raw JSON is import/export only, under Advanced.
#
# NB: the builder is NOT wrapped in an outer expander — per-step Configure
# panels are expanders, and Streamlit forbids nesting expanders.


def _seed_steps_from(pipeline) -> None:
    """Replace the session step list from a Pipeline, assigning fresh ids."""
    seq = st.session_state.get("pipeline_step_seq", 0)
    steps: list[dict] = []
    for s in pipeline.steps:
        steps.append({
            "id": seq, "tool": s.tool,
            "enabled": s.enabled, "options": dict(s.options),
        })
        seq += 1
    st.session_state["pipeline_steps"] = steps
    st.session_state["pipeline_step_seq"] = seq


if "pipeline_steps" not in st.session_state:
    _seed_steps_from(recommended_pipeline())

st.subheader("Build your pipeline")

mode = st.radio(
    "How would you like to define the pipeline?",
    [
        "Use the recommended default (Clean Text → Standardize → Fix Missing → Find Duplicates)",
        "Build interactively",
        "Import a saved pipeline JSON",
    ],
    index=0,
    key="pipeline_mode",
)

if mode.startswith("Use the recommended"):
    # Only reseed on an explicit click that lands here while the steps already
    # diverge — otherwise every rerun would wipe edits. We detect "user just
    # selected this mode" by comparing against the recommended default and
    # offering a one-click restore rather than silently discarding.
    rec_dict = recommended_pipeline().to_dict()
    cur_dict = {
        "steps": [
            {"tool": s["tool"], "options": s["options"],
             "enabled": s["enabled"], "name": None}
            for s in st.session_state["pipeline_steps"]
        ]
    }
    if cur_dict != rec_dict:
        st.info(
            "You've edited the recommended steps, so they're now yours to "
            "change — you're effectively in **Build interactively** mode. "
            "Restore the suggested steps to discard your edits."
        )
        if st.button("↺ Restore recommended steps"):
            _seed_steps_from(recommended_pipeline())
            st.rerun()
elif mode.startswith("Import"):
    pipeline_file = st.file_uploader(
        "Pipeline JSON", type=["json"], key="pipeline_upload",
    )
    if pipeline_file is not None:
        try:
            data = json.loads(pipeline_file.getvalue())
            _seed_steps_from(Pipeline.from_dict(data))
            st.success(
                f"Loaded {len(st.session_state['pipeline_steps'])} step(s). "
                "Switch to **Build interactively** to tweak them."
            )
        except Exception as e:
            from src.core.errors import format_for_user
            st.error(f"**Could not parse pipeline**\n\n```\n{format_for_user(e)}\n```")

st.caption(
    "Each step is a module: toggle it on/off, reorder with ▲ ▼, remove with ✕, "
    "and open **Configure** to set its options in plain language. Tool order is "
    "recommended, not enforced — violations surface as warnings below."
)

# Render the module stack. A reorder/remove action mutates the list and reruns.
steps = st.session_state["pipeline_steps"]
total = len(steps)
pending_action: tuple[str, int] | None = None
for i, step in enumerate(steps):
    act = render_step_card(df, step, i, total)
    if act is not None:
        pending_action = (act, i)

if pending_action is not None:
    act, i = pending_action
    if act == "remove":
        steps.pop(i)
    elif act == "up" and i > 0:
        steps[i - 1], steps[i] = steps[i], steps[i - 1]
    elif act == "down" and i < total - 1:
        steps[i + 1], steps[i] = steps[i], steps[i + 1]
    st.session_state["pipeline_steps"] = steps
    st.rerun()

# Add-step control.
add_col, btn_col = st.columns([0.7, 0.3])
with add_col:
    add_tool = st.selectbox(
        "Add a step",
        TOOL_NAMES,
        format_func=step_label,
        key="pipeline_add_tool",
        label_visibility="collapsed",
    )
with btn_col:
    if st.button("➕ Add step", width="stretch"):
        seq = st.session_state.get("pipeline_step_seq", 0)
        steps.append({"id": seq, "tool": add_tool, "enabled": True, "options": {}})
        st.session_state["pipeline_step_seq"] = seq + 1
        st.rerun()

# Build a Pipeline object from the step list.
steps_list: list[Step] = []
parse_errors: list[str] = []
for i, step in enumerate(steps):
    try:
        steps_list.append(Step(
            tool=str(step["tool"]),
            options=dict(step.get("options") or {}),
            enabled=bool(step.get("enabled", True)),
        ))
    except Exception as e:
        parse_errors.append(f"Step {i + 1} ({step.get('tool')}): {e}")

for err in parse_errors:
    st.error(err)

current_pipeline = Pipeline(steps=steps_list) if steps_list else None

if current_pipeline is not None:
    warnings = validate_pipeline(current_pipeline)
    if warnings:
        st.warning(
            "Pipeline is out of recommended order:\n\n"
            + "\n".join(f"- {w}" for w in warnings)
            + "\n\nThe pipeline will still run — these are recommendations only."
        )

with st.expander("Recommended tool order — why each step belongs where it does"):
    st.markdown(
        "\n".join(
            f"- **{step_label(e)}** before **{step_label(l)}** — {why}"
            for e, l, why in SOFT_DEPENDENCIES
        )
    )

with st.expander("Advanced — import / export pipeline as JSON"):
    st.caption(
        "For sharing or version control. Editing is done in the step panels "
        "above — this is just the saved form of the same settings. The same "
        "JSON runs in the CLI via `--pipeline pipeline.json`."
    )
    export_json = json.dumps(
        current_pipeline.to_dict() if current_pipeline else {"steps": []},
        indent=2, default=str,
    )
    st.code(export_json, language="json")
    adv_paste = st.text_area(
        "Paste pipeline JSON to load it", key="pipeline_json_paste", height=140,
    )
    if st.button("Load pasted JSON", disabled=not adv_paste.strip()):
        try:
            _seed_steps_from(Pipeline.from_dict(json.loads(adv_paste)))
            st.success("Loaded. Scroll up to see the steps.")
            st.rerun()
        except Exception as e:
            from src.core.errors import format_for_user
            st.error(f"**Could not parse pipeline**\n\n```\n{format_for_user(e)}\n```")

st.divider()

# ---------------------------------------------------------------------------
# Run
# ---------------------------------------------------------------------------

run_disabled = current_pipeline is None or not current_pipeline.steps

if st.button(
    "Run Pipeline",
    type="primary",
    width="stretch",
    disabled=run_disabled,
):
    progress = st.progress(0.0, text="Starting...")
    log_box = st.empty()
    log_lines: list[str] = []
    total_enabled = sum(1 for s in current_pipeline.steps if s.enabled)
    completed = [0]

    def _on_step(sr) -> None:
        completed[0] += 1
        if sr.skipped:
            log_lines.append(f"○ {step_label(sr.step.tool)} (skipped)")
        elif sr.error:
            log_lines.append(
                f"✗ {step_label(sr.step.tool)} — {sr.error.splitlines()[0]}"
            )
        else:
            log_lines.append(
                f"✓ {step_label(sr.step.tool)} — {sr.elapsed_seconds*1000:.0f} ms"
            )
        log_box.markdown("\n".join(log_lines))
        progress.progress(
            completed[0] / max(total_enabled, 1),
            text=f"Step {completed[0]}/{total_enabled}",
        )

    try:
        result = run_pipeline(
            df, current_pipeline,
            on_step_complete=_on_step,
            stop_on_error=False,
        )
    except Exception as e:
        from src.core.errors import format_for_user
        st.error(f"**Pipeline halted**\n\n```\n{format_for_user(e)}\n```")
        st.stop()

    progress.progress(1.0, text="Done")
    st.session_state["pipeline_result"] = result
    from src.audit import log_event
    log_event("tool_run", "Automated Workflows run", page="9_Pipeline_Runner")
    st.session_state["pipeline_input_name"] = uploaded.name
    # One-shot flag picked up on the next pass to scroll the parent
    # document to the Results anchor (see scroll snippet at end of file).
    st.session_state["_pipeline_scroll_to_results"] = True
    # Force a second rerun so the preview and options expanders see
    # the new result on the NEXT script pass and collapse themselves.
    # Without this they stay expanded until the user touches any
    # other widget.
    st.rerun()

result = st.session_state.get("pipeline_result")
if result is None:
    st.info(
        "Configure the pipeline above and click **Run Pipeline** to "
        "execute it on your file."
    )
    st.stop()

# ---------------------------------------------------------------------------
# Results
# ---------------------------------------------------------------------------

# Anchor target for the auto-scroll snippet at the end of this block.
# A bare ``<div id="...">`` survives Streamlit's HTML sanitizer (only
# ``<script>`` is stripped), and a 1px-tall div doesn't visually shift
# anything. Placed before the subheader so the scrolled-to viewport
# starts a few pixels above the section heading rather than below it.
st.markdown(
    '<div id="pipeline-results-anchor" style="height:1px"></div>',
    unsafe_allow_html=True,
)

st.subheader("Results")

m1, m2, m3, m4 = st.columns(4)
m1.metric("Initial rows", result.initial_rows)
m2.metric("Final rows", result.final_rows)
m3.metric("Steps run", sum(1 for s in result.step_results if not s.skipped))
m4.metric("Elapsed", f"{result.total_elapsed:.2f} s")

st.markdown("**Per-step summary**")
# Plain-English status pill + summary phrase per step (mockup §Results). The
# at-a-glance table stays scannable; any warn/error step also gets an inline
# detail callout directly below it, so a non-fatal issue surfaces in context
# without a dedicated always-empty column.
step_df = pd.DataFrame([
    {
        "step": step_label(sr.step.tool),
        "status": step_status(
            sr.step.tool, sr.summary, skipped=sr.skipped, error=sr.error,
        )[0],
        "elapsed": f"{int(sr.elapsed_seconds * 1000)} ms",
        "summary": (
            "—" if sr.skipped
            else step_phrase(sr.step.tool, sr.summary)
        ),
    }
    for sr in result.step_results
])
st.dataframe(step_df, width="stretch", hide_index=True)

for sr in result.step_results:
    _label, level, detail = step_status(
        sr.step.tool, sr.summary, skipped=sr.skipped, error=sr.error,
    )
    if not detail:
        continue
    name = step_label(sr.step.tool)
    if level == "error":
        st.error(f"**{name}** — {detail}")
    else:
        st.warning(f"**{name}** — {detail}")

st.markdown("**Output preview (first 10 rows)**")
st.dataframe(result.final_df.head(10), width="stretch")

# ---------------------------------------------------------------------------
# Downloads
# ---------------------------------------------------------------------------
#
# All three byte buffers are prepared up front (outside the columns) so
# each ``st.download_button`` sees stable ``data`` across reruns and an
# explicit ``key`` — without those, Streamlit auto-derived widget IDs
# can collide for multiple download_buttons in adjacent columns and
# only the first one actually fires on click. The pipeline-JSON button
# now renders unconditionally (disabled when no pipeline is defined)
# so the layout stays steady.

st.divider()
stem = Path(st.session_state.get("pipeline_input_name", "input")).stem

cleaned_bytes = result.final_df.to_csv(index=False).encode("utf-8-sig")
pipeline_bytes = json.dumps(
    current_pipeline.to_dict() if current_pipeline else {"steps": []},
    indent=2, default=str,
).encode("utf-8")
audit_bytes = json.dumps({
    "warnings": result.warnings,
    "initial_rows": result.initial_rows,
    "final_rows": result.final_rows,
    "total_elapsed_seconds": result.total_elapsed,
    "steps": [
        {
            "tool": sr.step.tool,
            "name": sr.step.display_name(),
            "enabled": sr.step.enabled,
            "skipped": sr.skipped,
            "elapsed_seconds": sr.elapsed_seconds,
            "summary": sr.summary,
            "error": sr.error,
        }
        for sr in result.step_results
    ],
}, indent=2, default=str).encode("utf-8")

_pipeline_empty = current_pipeline is None or not current_pipeline.steps

dl_a, dl_b, dl_c = st.columns(3)
with dl_a:
    html_download_button(
        "Download cleaned CSV",
        cleaned_bytes,
        file_name=f"{stem}_pipeline.csv",
        mime="text/csv",
    )
with dl_b:
    html_download_button(
        "Download pipeline JSON",
        pipeline_bytes,
        file_name="pipeline.json",
        mime="application/json",
        disabled=_pipeline_empty,
        help=(
            "No pipeline defined."
            if _pipeline_empty
            else "Save this and pass --pipeline pipeline.json to the CLI to re-run on next week's file."
        ),
    )
with dl_c:
    html_download_button(
        "Download run audit",
        audit_bytes,
        file_name=f"{stem}_pipeline_audit.json",
        mime="application/json",
    )


# ---------------------------------------------------------------------------
# Post-run auto-scroll
# ---------------------------------------------------------------------------
#
# When the user clicks Run Pipeline, the preview + options collapse but
# Streamlit by itself doesn't scroll — the Results section is at the
# bottom of a tall script so the user has to find it. Inject a tiny
# component-html iframe that calls ``scrollIntoView`` on the parent's
# Results anchor. Streamlit's main page is same-origin with component
# iframes so ``window.parent.document`` access is allowed.
#
# The flag is one-shot (``pop`` removes it) so re-renders triggered by
# unrelated widgets in the Results section don't yank the viewport
# back to the top of Results.
if st.session_state.pop("_pipeline_scroll_to_results", False):
    st.iframe(
        """
        <script>
          const doc = window.parent.document;
          const target = doc.getElementById('pipeline-results-anchor');
          if (target) target.scrollIntoView({behavior: 'smooth', block: 'start'});
        </script>
        """,
        height=1,
    )
