#
#   Imandra Inc.
#
#   speclogician/commands/ch_cmd.py
#

import typer
from typing import Optional
from pathlib import Path
from ..state.change import (
    DomainModelBaseEdit,
    PredicateAdd,
    PredicateEdit,
    PredicateRemove,
    TransitionAdd,
    TransitionEdit,
    TransitionRemove,
    ScenarioAdd,
    ScenarioEdit,
    ScenarioRemove,
    AddTestTrace,
    EditTestTrace,
    AddLogTrace,
    EditLogTrace,
    RemoveTraceArtifact,
    AddDocRef,
    EditDocRef,
    AddSrcCodeRef,
    EditSrcCodeRef,
    RemoveDataArtifact,
    LinkArtifactsComponents,
    RemoveArtComponentLink
)

from ..modeling.scenario import ScenarioDelta, BoolDelta
from ..modeling.predicates import PredicateType
from .utils import (
    parse_scenario_delta,
    get_state
)
from .runner import run_change_and_render

app = typer.Typer(help="Change state commands")


#-----------------------------------------------------------------------------
#   Base Model
#-----------------------------------------------------------------------------

@app.command("base-edit")
def domain_base_edit(
    ctx: typer.Context,
    src: Optional[str] = typer.Option(
        None,
        "--src",
        help="IML source code for the domain base component",
    ),
    file: Optional[Path] = typer.Option(
        None,
        "--file",
        exists=True,
        readable=True,
        help="Path to a file containing IML base source code",
    ),
    json_only: bool = typer.Option(
        False,
        "--json",
        help="Emit JSON only (no rich output)",
    ),
):
    """
    Edit the base component of the domain model.
    """

    state = get_state(ctx)

    # --- input validation ---
    if src is None and file is None:
        raise typer.BadParameter("One of --src or --file must be provided")

    if src is not None and file is not None:
        raise typer.BadParameter("Only one of --src or --file may be provided")

    if file is not None:
        src = file.read_text()

    assert src is not None  # for type checkers

    change = DomainModelBaseEdit(new_base_src=src)

    run_change_and_render(state, change, json_only=json_only)
    state.save()

#-----------------------------------------------------------------------------
#   Scenarios
#-----------------------------------------------------------------------------
@app.command(name="scenario-add")
def scenario_add(
    ctx: typer.Context,
    scenario_name: str = typer.Argument(
        ...,
        help="Name of the scenario to add",
    ),
    given: list[str] = typer.Option(
        [],
        "--given",
        help=(
            "State predicate name (repeatable).\n"
            "Examples:\n"
            "  --given pred_eq_ten\n"
            "  --given pred_gte_ten --given pred_ne_ten"
        ),
    ),
    when: list[str] = typer.Option(
        [],
        "--when",
        help=(
            "Action predicate name (repeatable).\n"
            "Examples:\n"
            "  --when act_gt_x\n"
            "  --when act_gt_x --when act_lte_x"
        ),
    ),
    then: list[str] = typer.Option(
        [],
        "--then",
        help=(
            "Transition name (repeatable).\n"
            "Examples:\n"
            "  --then step1\n"
            "  --then step1 --then step2"
        ),
    ),
    given_list: str | None = typer.Option(
        None,
        "--given-list",
        help=(
            "Comma-separated list of state predicates.\n"
            'Example: --given-list "pred_eq_ten,pred_ne_ten"'
        ),
    ),
    when_list: str | None = typer.Option(
        None,
        "--when-list",
        help=(
            "Comma-separated list of action predicates.\n"
            'Example: --when-list "act_gt_x,act_lte_x"'
        ),
    ),
    then_list: str | None = typer.Option(
        None,
        "--then-list",
        help=(
            "Comma-separated list of transitions.\n"
            'Example: --then-list "step1,step2"'
        ),
    ),
    initial: bool | None = typer.Option(
        None,
        "--initial/--no-initial",
        help="Mark this scenario as an initial scenario.",
    ),
    json_only: bool = typer.Option(
        False,
        "--json",
        help="Output the parsed ScenarioAdd as JSON",
    ),
    ) -> None:

        state = get_state(ctx)

        def _split_csv(s: str | None) -> list[str]:
            if not s:
                return []
            return [x.strip() for x in s.split(",") if x.strip()]

        # Merge repeatable flags + csv lists (dedup preserving order)
        def _merge(a: list[str], b: list[str]) -> list[str]:
            return list(dict.fromkeys([*a, *b]))

        given_all = _merge(given, _split_csv(given_list))
        when_all = _merge(when, _split_csv(when_list))
        then_all = _merge(then, _split_csv(then_list))

        def _delta_from_add(xs: list[str]) -> ScenarioDelta | None:
            d = ScenarioDelta(add=xs)
            return None if d.is_empty() else d

        is_initial = None
        if initial is not None:
            is_initial = BoolDelta(set=initial)

        change = ScenarioAdd(
            scenario_name=scenario_name,
            given=_delta_from_add(given_all),
            when=_delta_from_add(when_all),
            then=_delta_from_add(then_all),
            is_initial=is_initial
        )

        run_change_and_render(state, change, json_only=json_only)
        state.save()
    

@app.command(name="scenario-edit", help="Edit an existing scenario by adding/removing predicates and transitions")
def scenario_edit_cmd(
    ctx: typer.Context,
    scenario_name: str = typer.Argument(
        ...,
        help="Name of the scenario to edit",
    ),
    given: str | None = typer.Option(
        None,
        "--given",
        help=(
            "Delta for GIVEN (state predicates).\n"
            "Formats:\n"
            "  '+p1,+p2,-p3'  add/remove predicates\n"
            "  'p1,p2'        add predicates\n"
            "  JSON string or path to JSON file\n"
            "Example: --given '+pred_eq_ten,-pred_lt_ten'"
        ),
    ),
    when: str | None = typer.Option(
        None,
        "--when",
        help=(
            "Delta for WHEN (action predicates).\n"
            "Formats:\n"
            "  '+q1,+q2,-q3'  add/remove predicates\n"
            "  'q1,q2'        add predicates\n"
            "  JSON string or path to JSON file\n"
            "Example: --when '+act_gt_x,-act_lte_x'"
        ),
    ),
    then: str | None = typer.Option(
        None,
        "--then",
        help=(
            "Delta for THEN (transition names).\n"
            "Formats:\n"
            "  '+t1,+t2,-t3'  add/remove transitions\n"
            "  't1,t2'        add transitions\n"
            "  JSON string or path to JSON file\n"
            "Example: --then '+step1,-step2'"
        ),
    ),
    initial: bool | None = typer.Option(
        None,
        "--initial/--no-initial",
        help="Mark this scenario as an initial scenario.",
    ),
    json_only: bool = typer.Option(
        False,
        "--json",
        help="Output the parsed ScenarioEdit as JSON",
    ),
    ):
    
    state = get_state(ctx)

    is_initial = None
    if initial is not None:
        is_initial = BoolDelta(set=initial)

    change = ScenarioEdit(
        scenario_name=scenario_name,
        given=parse_scenario_delta(given),
        when=parse_scenario_delta(when),
        then=parse_scenario_delta(then),
        is_initial=is_initial
    )

    run_change_and_render(state, change, json_only=json_only)
    state.save()

@app.command(name="scenario-remove", help="Remove a scenario from the current spec/state.")
def scenario_remove_cmd(
    ctx: typer.Context,
    scenario_name: str = typer.Argument(
        ...,
        help="Name of the scenario to remove (identifier). Example: scenario_1",
    ),
    json_only: bool = typer.Option(
        False,
        "--json",
        help="Output machine-readable JSON (includes change + state summaries + diff).",
    ),
) -> None:
    """
    Examples:

      speclogician scenario remove scenario_1

      speclogician --state-json /path/to/project scenario remove scenario_1 --json

      speclogician scenario remove scenario_1 -y
    """
    state = get_state(ctx)
    change = ScenarioRemove(scenario_name=scenario_name)
    run_change_and_render(state, change, json_only=json_only)
    state.save()


#-----------------------------------------------------------------------------
#   Traces (TestTrace and LogTrace)
#-----------------------------------------------------------------------------

trace_app = typer.Typer(help="Trace artifacts (TestTrace, LogTrace).")
app.add_typer(trace_app, name="trace")


@app.command("test-add")
def add_test_trace(
    ctx: typer.Context,
    art_id: str = typer.Option(..., "--art-id", help="Artifact ID (uuid string)"),
    name: str = typer.Option(..., "--name", help="Test name (NOT unique)"),
    given: str = typer.Option(..., "--given", help="Concrete state expression, e.g. { x = 1 }"),
    when: Optional[str] = typer.Option(None, "--when", help="Concrete action expression"),
    then: Optional[str] = typer.Option(None, "--then", help="Concrete resulting state expression"),
    filepath: str = typer.Option("", "--filepath", help="Original test filepath"),
    language: Optional[str] = typer.Option(None, "--language", help="Language"),
    contents: Optional[str] = typer.Option(None, "--contents", help="Original test contents"),
    json_only: bool = typer.Option(False, "--json", help="Emit JSON result (no rich output)"),
) -> None:
    state = get_state(ctx)
    change = AddTestTrace(
        art_id=art_id,
        name=name,
        given=given,
        when=when,
        then=then,
        filepath=filepath,
        language=language,
        contents=contents,
    )

    run_change_and_render(state, change, json_only=json_only)
    state.save()

@app.command("test-edit")
def edit_test_trace(
    ctx: typer.Context,
    art_id: str = typer.Argument(..., help="Artifact ID"),
    name: Optional[str] = typer.Option(None, "--name"),
    given: Optional[str] = typer.Option(None, "--given"),
    when: Optional[str] = typer.Option(None, "--when"),
    then: Optional[str] = typer.Option(None, "--then"),
    filepath: Optional[str] = typer.Option(None, "--filepath"),
    language: Optional[str] = typer.Option(None, "--language"),
    contents: Optional[str] = typer.Option(None, "--contents"),
    json_only: bool = typer.Option(False, "--json"),
) -> None:
    state = get_state(ctx)
    change = EditTestTrace(
        art_id=art_id,
        name=name,
        given=given,
        when=when,
        then=then,
        filepath=filepath,
        language=language,
        contents=contents,
    )

    run_change_and_render(state, change, json_only=json_only)
    state.save()

@app.command("log-add")
def add_log_trace(
    ctx: typer.Context,
    art_id: str = typer.Option(..., "--art-id", help="Artifact ID (uuid string)"),
    filename: str = typer.Option(..., "--filename", help="Log filename (NOT unique)"),
    contents: str = typer.Option(..., "--contents", help="Log contents"),
    given: str = typer.Option(..., "--given", help="Concrete state expression"),
    when: Optional[str] = typer.Option(None, "--when", help="Concrete action expression"),
    then: Optional[str] = typer.Option(None, "--then", help="Concrete resulting state expression"),
    json_only: bool = typer.Option(False, "--json"),
) -> None:
    state = get_state(ctx)
    change = AddLogTrace(
        art_id=art_id,
        filename=filename,
        contents=contents,
        given=given,
        when=when,
        then=then,
    )

    run_change_and_render(state, change, json_only=json_only)
    state.save()

@app.command("log-edit")
def edit_log_trace(
    ctx: typer.Context,
    art_id: str = typer.Argument(..., help="Artifact ID"),
    filename: Optional[str] = typer.Option(None, "--filename"),
    contents: Optional[str] = typer.Option(None, "--contents"),
    given: Optional[str] = typer.Option(None, "--given"),
    when: Optional[str] = typer.Option(None, "--when"),
    then: Optional[str] = typer.Option(None, "--then"),
    json_only: bool = typer.Option(False, "--json"),
) -> None:
    state = get_state(ctx)
    change = EditLogTrace(
        art_id=art_id,
        filename=filename,
        contents=contents,
        given=given,
        when=when,
        then=then,
    )
    run_change_and_render(state, change, json_only=json_only)
    state.save()


@app.command("trace-remove")
def remove_trace(
    ctx: typer.Context,
    art_id: str = typer.Argument(..., help="Artifact ID"),
    json_only: bool = typer.Option(False, "--json"),
) -> None:
    state = get_state(ctx)
    change = RemoveTraceArtifact(art_id=art_id)
    run_change_and_render(state, change, json_only=json_only)
    state.save()


# --------------------------------------------------------
# Data artifacts (DocRefs and SrcRefs)
# --------------------------------------------------------

@app.command("add-doc")
def add_doc(
    ctx: typer.Context,
    art_id: str = typer.Option(..., "--art-id"),
    text: str = typer.Option(..., "--text"),
    meta: Optional[str] = typer.Option(None, "--meta"),
    json_only: bool = typer.Option(False, "--json"),
) -> None:
    state = get_state(ctx)
    change = AddDocRef(art_id=art_id, text=text, meta=meta)
    run_change_and_render(state, change, json_only=json_only)
    state.save()


@app.command("edit-doc")
def edit_doc(
    ctx: typer.Context,
    art_id: str = typer.Argument(...),
    text: Optional[str] = typer.Option(None, "--text"),
    meta: Optional[str] = typer.Option(None, "--meta"),
    json_only: bool = typer.Option(False, "--json"),
) -> None:
    state = get_state(ctx)
    change = EditDocRef(art_id=art_id, text=text, meta=meta)
    run_change_and_render(state, change, json_only=json_only)
    state.save()


@app.command("add-src")
def add_src(
    ctx: typer.Context,
    art_id: str = typer.Option(..., "--art-id"),
    src_code: str = typer.Option(..., "--src-code"),
    language: Optional[str] = typer.Option(None, "--language"),
    file_path: Optional[str] = typer.Option(None, "--file-path"),
    iml_code: Optional[str] = typer.Option(None, "--iml-code"),
    meta: Optional[str] = typer.Option(None, "--meta"),
    json_only: bool = typer.Option(False, "--json"),
) -> None:
    state = get_state(ctx)
    change = AddSrcCodeRef(
        art_id=art_id,
        src_code=src_code,
        language=language,
        file_path=file_path,
        iml_code=iml_code,
        meta=meta,
    )
    run_change_and_render(state, change, json_only=json_only)
    state.save()


@app.command("edit-src")
def edit_src(
    ctx: typer.Context,
    art_id: str = typer.Argument(...),
    src_code: Optional[str] = typer.Option(None, "--src-code"),
    language: Optional[str] = typer.Option(None, "--language"),
    file_path: Optional[str] = typer.Option(None, "--file-path"),
    iml_code: Optional[str] = typer.Option(None, "--iml-code"),
    meta: Optional[str] = typer.Option(None, "--meta"),
    json_only: bool = typer.Option(False, "--json"),
) -> None:
    state = get_state(ctx)
    change = EditSrcCodeRef(
        art_id=art_id,
        src_code=src_code,
        language=language,
        file_path=file_path,
        iml_code=iml_code,
        meta=meta,
    )
    run_change_and_render(state, change, json_only=json_only)
    state.save()


@app.command("remove-art")
def remove_data_art(
    ctx: typer.Context,
    art_id: str = typer.Argument(...),
    json_only: bool = typer.Option(False, "--json"),
) -> None:
    state = get_state(ctx)
    change = RemoveDataArtifact(art_id=art_id)
    run_change_and_render(state, change, json_only=json_only)
    state.save()


#-----------------------------------------------------------------------------
#   Predicates
#-----------------------------------------------------------------------------

@app.command("pred-add")
def predicate_add(
    ctx: typer.Context,
    pred_type: str = typer.Option(..., "--pred-type", help="STATE or ACTION"),
    pred_name: str = typer.Option(..., "--pred-name", help="Predicate name"),
    pred_src: str = typer.Option(..., "--pred-src", help="Predicate source code"),
    json_only: bool = typer.Option(False, "--json", help="Emit JSON output"),
):
    """
    Add a new predicate to the domain model.
    """
    state = get_state(ctx)

    pt = (pred_type or "").strip().lower()
    if pt not in ("state", "action"):
        raise typer.BadParameter("--pred-type must be one of: state, action")

    change = PredicateAdd(
        pred_type=PredicateType(pt),
        pred_name=pred_name,
        pred_src=pred_src,
    )
    run_change_and_render(state, change, json_only=json_only)
    state.save()


@app.command("pred-edit")
def predicate_edit(
    ctx: typer.Context,
    pred_name: str = typer.Option(..., "--pred-name", help="Predicate name"),
    pred_src: str = typer.Option(..., "--pred-src", help="Updated predicate source"),
    json_only: bool = typer.Option(False, "--json", help="Emit JSON output"),
):
    """
    Edit an existing predicate.
    """
    state = get_state(ctx)

    change = PredicateEdit(
        pred_name=pred_name,
        pred_src=pred_src,
    )
    run_change_and_render(state, change, json_only=json_only)
    state.save()

@app.command("pred-remove")
def predicate_remove(
    ctx: typer.Context,
    pred_name: str = typer.Option(..., "--pred-name", help="Predicate name to remove"),
    json_only: bool = typer.Option(False, "--json", help="Emit JSON output"),
):
    """
    Remove a predicate from the domain model.
    """
    state = get_state(ctx)
    change = PredicateRemove(pred_name=pred_name)
    run_change_and_render(state, change, json_only=json_only)
    state.save()

#-----------------------------------------------------------------------------
#   Transitions
#-----------------------------------------------------------------------------

@app.command("trans-add")
def transition_add(
    ctx: typer.Context,
    trans_name: str = typer.Option(..., "--trans-name", help="Transition name"),
    trans_src: str = typer.Option(..., "--trans-src", help="Transition source code"),
    json_only: bool = typer.Option(False, "--json", help="Emit JSON output"),
):
    """
    Add a new transition function.
    """
    state = get_state(ctx)
    change = TransitionAdd(
        trans_name=trans_name,
        trans_src=trans_src,
    )
    run_change_and_render(state, change, json_only=json_only)
    state.save()


@app.command("trans-edit")
def transition_edit(
    ctx: typer.Context,
    trans_name: str = typer.Option(..., "--trans-name", help="Transition name"),
    trans_src: str = typer.Option(..., "--trans-src", help="Updated transition source"),
    json_only: bool = typer.Option(False, "--json", help="Emit JSON output"),
):
    """
    Edit an existing transition function.
    """
    state = get_state(ctx)
    change = TransitionEdit(
        trans_name=trans_name,
        trans_src=trans_src,
    )
    run_change_and_render(state, change, json_only=json_only)
    state.save()

@app.command("trans-remove")
def transition_remove(
    ctx: typer.Context,
    trans_name: str = typer.Option(..., "--trans-name", help="Transition name to remove"),
    json_only: bool = typer.Option(False, "--json", help="Emit JSON output"),
):
    """
    Remove a transition function.
    """
    state = get_state(ctx)
    change = TransitionRemove(trans_name=trans_name)
    run_change_and_render(state, change, json_only=json_only)
    state.save()


#-----------------------------------------------------------------------------
#   Artifact Mapping
#-----------------------------------------------------------------------------
