from __future__ import annotations

import time
from pathlib import Path
from typing import Any

from simplicity_cli.client import SimplicityApiClient
from simplicity_cli.errors import CliError, ExitCode
from simplicity_cli.models import DownloadResolution, JSONDict
from simplicity_cli.output import OutputReporter


def load_optional_text(value: str | None, file_path: Path | None, label: str) -> str | None:
    if value is not None and file_path is not None:
        raise CliError(
            f"use either --{label} or --{label}-file, not both.",
            exit_code=ExitCode.USAGE_ERROR,
            code="invalid_input",
        )
    if file_path is None:
        return value
    try:
        return file_path.read_text(encoding="utf-8")
    except OSError as exc:
        raise CliError(
            f"failed to read {label} file: {file_path}",
            exit_code=ExitCode.USAGE_ERROR,
            code="file_read_error",
            details={"path": str(file_path), "error": str(exc)},
        ) from exc


def poll_task(
    client: SimplicityApiClient,
    task_id: str,
    poll_interval_seconds: float,
    max_wait_seconds: float,
    reporter: OutputReporter | None = None,
    task_label: str = "task",
) -> JSONDict:
    if poll_interval_seconds <= 0:
        raise CliError(
            "poll interval must be greater than 0 seconds.",
            exit_code=ExitCode.USAGE_ERROR,
            code="invalid_poll_interval",
        )
    if max_wait_seconds <= 0:
        raise CliError(
            "max wait must be greater than 0 seconds.",
            exit_code=ExitCode.USAGE_ERROR,
            code="invalid_max_wait",
        )

    start = time.monotonic()
    last_status: str | None = None
    last_progress_reported: int | None = None
    last_emit = 0.0
    not_found_reported = False
    while True:
        elapsed_seconds = time.monotonic() - start
        try:
            task = client.get_task(task_id)
        except CliError as exc:
            if _is_transient_task_not_found(exc):
                if reporter is not None and not not_found_reported:
                    reporter.task_update(
                        label=task_label,
                        task_id=task_id,
                        status="not-found-yet",
                        progress=None,
                        elapsed_seconds=elapsed_seconds,
                    )
                    not_found_reported = True
                if elapsed_seconds >= max_wait_seconds:
                    raise CliError(
                        f"task {task_id} did not become available within {max_wait_seconds:.0f} seconds.",
                        exit_code=ExitCode.TIMEOUT,
                        code="task_timeout",
                        details={"task_id": task_id},
                    ) from exc
                time.sleep(poll_interval_seconds)
                continue
            raise
        status = str(task.get("status", ""))
        progress = _extract_progress(task)
        if reporter is not None and _should_emit_task_update(
            status=status,
            progress=progress,
            last_status=last_status,
            last_progress_reported=last_progress_reported,
            elapsed_seconds=elapsed_seconds,
            last_emit=last_emit,
        ):
            reporter.task_update(
                label=task_label,
                task_id=task_id,
                status=status,
                progress=progress,
                elapsed_seconds=elapsed_seconds,
            )
            last_status = status
            last_progress_reported = progress
            last_emit = elapsed_seconds
        if status == "completed":
            return task
        if status == "failed":
            error_message = str(task.get("error_message") or "unknown task failure")
            raise CliError(
                f"task {task_id} failed: {error_message}",
                exit_code=ExitCode.TASK_FAILED,
                code="task_failed",
                details={"task_id": task_id, "task": task},
            )
        if elapsed_seconds >= max_wait_seconds:
            raise CliError(
                f"task {task_id} did not complete in {max_wait_seconds:.0f} seconds.",
                exit_code=ExitCode.TIMEOUT,
                code="task_timeout",
                details={"task_id": task_id},
            )
        time.sleep(poll_interval_seconds)


def run_fill_new(
    client: SimplicityApiClient,
    reporter: OutputReporter,
    *,
    form_file: Path | None,
    form_url: str | None,
    source_files: list[Path],
    source_urls: list[str],
    context: str | None,
    instructions: str | None,
    wait: bool,
    poll_interval_seconds: float,
    max_wait_seconds: float,
    download: bool,
    output_path: Path | None,
) -> JSONDict:
    reporter.phase("Upload form document")
    form_upload_task = (
        client.upload_document_file(form_file, document_type="FORM_DOCUMENT")
        if form_file is not None
        else client.upload_document_url(form_url or "", document_type="FORM_DOCUMENT")
    )
    form_upload_task_id = _required_task_id(form_upload_task, context_label="form upload")
    reporter.detail(f"task_id={form_upload_task_id}")

    reporter.phase("Wait for form upload")
    completed_form_upload = poll_task(
        client,
        form_upload_task_id,
        poll_interval_seconds=poll_interval_seconds,
        max_wait_seconds=max_wait_seconds,
        reporter=reporter,
        task_label="form upload",
    )
    form_document_id = _required_document_id(completed_form_upload, context_label="form upload")
    reporter.detail(f"form_document_id={form_document_id}")

    source_document_ids: list[str] = []
    source_total = len(source_files) + len(source_urls)
    source_index = 0
    for source_file in source_files:
        source_index += 1
        reporter.phase(f"Upload source document {source_index}/{source_total}")
        source_upload_task = client.upload_document_file(source_file, document_type="SOURCE_DOCUMENT")
        source_task_id = _required_task_id(source_upload_task, context_label="source upload")
        reporter.detail(f"task_id={source_task_id}")
        reporter.phase(f"Wait for source upload {source_index}/{source_total}")
        completed_source_upload = poll_task(
            client,
            source_task_id,
            poll_interval_seconds=poll_interval_seconds,
            max_wait_seconds=max_wait_seconds,
            reporter=reporter,
            task_label=f"source upload {source_index}/{source_total}",
        )
        source_document_id = _required_document_id(completed_source_upload, context_label="source upload")
        source_document_ids.append(source_document_id)
        reporter.detail(f"source_document_id={source_document_id}")

    for source_url in source_urls:
        source_index += 1
        reporter.phase(f"Upload source document {source_index}/{source_total}")
        source_upload_task = client.upload_document_url(source_url, document_type="SOURCE_DOCUMENT")
        source_task_id = _required_task_id(source_upload_task, context_label="source upload")
        reporter.detail(f"task_id={source_task_id}")
        reporter.phase(f"Wait for source upload {source_index}/{source_total}")
        completed_source_upload = poll_task(
            client,
            source_task_id,
            poll_interval_seconds=poll_interval_seconds,
            max_wait_seconds=max_wait_seconds,
            reporter=reporter,
            task_label=f"source upload {source_index}/{source_total}",
        )
        source_document_id = _required_document_id(completed_source_upload, context_label="source upload")
        source_document_ids.append(source_document_id)
        reporter.detail(f"source_document_id={source_document_id}")

    reporter.phase("Start autofill")
    autofill_task = client.create_autofill_form(
        form_file_document_id=form_document_id,
        source_document_ids=source_document_ids or None,
        context=context,
        instructions=instructions,
    )
    autofill_task_id = _required_task_id(autofill_task, context_label="autofill")
    reporter.detail(f"task_id={autofill_task_id}")

    if not wait:
        return _result_payload(
            status=str(autofill_task.get("status", "processing")),
            task_id=autofill_task_id,
            task=autofill_task,
        )

    reporter.phase("Wait for autofill")
    completed_autofill = poll_task(
        client,
        autofill_task_id,
        poll_interval_seconds=poll_interval_seconds,
        max_wait_seconds=max_wait_seconds,
        reporter=reporter,
        task_label="autofill",
    )
    form_id = _extract_form_id(completed_autofill)
    document_id = _extract_document_id(completed_autofill)
    if form_id:
        reporter.detail(f"form_id={form_id}")
    if document_id:
        reporter.detail(f"document_id={document_id}")
    payload = _result_payload(
        status=str(completed_autofill.get("status", "completed")),
        task_id=autofill_task_id,
        task=completed_autofill,
        form_id=form_id,
        document_id=document_id,
    )

    if not download:
        return payload

    reporter.phase("Download filled PDF")
    download_resolution = resolve_download_url(client, form_id=form_id, document_id=document_id)
    payload["download_url"] = download_resolution.url
    if not download_resolution.url:
        raise CliError(
            "autofill completed but no download URL was returned.",
            exit_code=ExitCode.DOWNLOAD_ERROR,
            code="missing_download_url",
            details=payload,
        )

    resolved_output_path = default_output_path(form_id=form_id, document_id=document_id, output_path=output_path)
    client.download_file(download_resolution.url, resolved_output_path)
    payload["download_path"] = str(resolved_output_path.resolve())
    reporter.detail(f"output={payload['download_path']}")
    return payload


def run_fill_existing(
    client: SimplicityApiClient,
    reporter: OutputReporter,
    *,
    form_id: str,
    context: str | None,
    instructions: str | None,
    wait: bool,
    poll_interval_seconds: float,
    max_wait_seconds: float,
    download: bool,
    output_path: Path | None,
) -> JSONDict:
    reporter.phase("Start autofill")
    autofill_task = client.autofill_existing_form(form_id=form_id, context=context, instructions=instructions)
    autofill_task_id = _required_task_id(autofill_task, context_label="autofill")
    reporter.detail(f"task_id={autofill_task_id}")

    if not wait:
        return _result_payload(
            status=str(autofill_task.get("status", "processing")),
            task_id=autofill_task_id,
            task=autofill_task,
            form_id=form_id,
        )

    reporter.phase("Wait for autofill")
    completed_autofill = poll_task(
        client,
        autofill_task_id,
        poll_interval_seconds=poll_interval_seconds,
        max_wait_seconds=max_wait_seconds,
        reporter=reporter,
        task_label="autofill",
    )
    document_id = _extract_document_id(completed_autofill)
    reporter.detail(f"form_id={form_id}")
    if document_id:
        reporter.detail(f"document_id={document_id}")
    payload = _result_payload(
        status=str(completed_autofill.get("status", "completed")),
        task_id=autofill_task_id,
        task=completed_autofill,
        form_id=form_id,
        document_id=document_id,
    )

    if not download:
        return payload

    reporter.phase("Download filled PDF")
    download_resolution = resolve_download_url(client, form_id=form_id, document_id=document_id)
    payload["download_url"] = download_resolution.url
    if not download_resolution.url:
        raise CliError(
            "autofill completed but no download URL was returned.",
            exit_code=ExitCode.DOWNLOAD_ERROR,
            code="missing_download_url",
            details=payload,
        )

    resolved_output_path = default_output_path(form_id=form_id, document_id=document_id, output_path=output_path)
    client.download_file(download_resolution.url, resolved_output_path)
    payload["download_path"] = str(resolved_output_path.resolve())
    reporter.detail(f"output={payload['download_path']}")
    return payload


def run_task_status(client: SimplicityApiClient, task_id: str) -> JSONDict:
    task = client.get_task(task_id)
    return _result_payload(
        status=str(task.get("status", "unknown")),
        task_id=task_id,
        task=task,
        form_id=_extract_form_id(task),
        document_id=_extract_document_id(task),
    )


def run_task_wait(
    client: SimplicityApiClient,
    reporter: OutputReporter,
    task_id: str,
    poll_interval_seconds: float,
    max_wait_seconds: float,
) -> JSONDict:
    reporter.phase("Wait for task completion")
    reporter.detail(f"task_id={task_id}")
    task = poll_task(
        client,
        task_id=task_id,
        poll_interval_seconds=poll_interval_seconds,
        max_wait_seconds=max_wait_seconds,
        reporter=reporter,
        task_label="task wait",
    )
    return _result_payload(
        status=str(task.get("status", "completed")),
        task_id=task_id,
        task=task,
        form_id=_extract_form_id(task),
        document_id=_extract_document_id(task),
    )


def resolve_download_url(
    client: SimplicityApiClient,
    *,
    form_id: str | None,
    document_id: str | None,
) -> DownloadResolution:
    form_payload: JSONDict | None = None
    if form_id:
        form_payload = client.get_form(form_id)
        form_url = form_payload.get("form_url")
        if isinstance(form_url, str) and form_url:
            return DownloadResolution(url=form_url, form_payload=form_payload)

    if document_id:
        document_payload = client.get_document(document_id)
        presigned_url = document_payload.get("presigned_url")
        if isinstance(presigned_url, str) and presigned_url:
            return DownloadResolution(
                url=presigned_url,
                form_payload=form_payload,
                document_payload=document_payload,
            )
        return DownloadResolution(url=None, form_payload=form_payload, document_payload=document_payload)

    return DownloadResolution(url=None, form_payload=form_payload)


def default_output_path(form_id: str | None, document_id: str | None, output_path: Path | None) -> Path:
    if output_path is not None:
        return output_path

    if form_id:
        token = form_id
    elif document_id:
        token = document_id
    else:
        raise CliError(
            "cannot determine output filename because both form_id and document_id are missing.",
            exit_code=ExitCode.DOWNLOAD_ERROR,
            code="missing_identifiers",
        )
    return Path.cwd() / f"{token}.filled.pdf"


def _required_task_id(payload: JSONDict, context_label: str) -> str:
    task_id = payload.get("task_id")
    if isinstance(task_id, str) and task_id:
        return task_id
    raise CliError(
        f"{context_label} response did not include task_id.",
        exit_code=ExitCode.API_ERROR,
        code="invalid_api_response",
        details={"response": payload},
    )


def _required_document_id(task: JSONDict, context_label: str) -> str:
    document_id = _extract_document_id(task)
    if document_id:
        return document_id
    raise CliError(
        f"{context_label} task completed without document_id.",
        exit_code=ExitCode.API_ERROR,
        code="missing_document_id",
        details={"task": task},
    )


def _extract_form_id(task: JSONDict) -> str | None:
    result_data = task.get("result_data")
    if isinstance(result_data, dict):
        for field in ("form_id", "formId"):
            value = result_data.get(field)
            if isinstance(value, str) and value:
                return value
    form_id = task.get("form_id")
    if isinstance(form_id, str) and form_id:
        return form_id
    return None


def _extract_document_id(task: JSONDict) -> str | None:
    result_data = task.get("result_data")
    if isinstance(result_data, dict):
        value = result_data.get("document_id")
        if isinstance(value, str) and value:
            return value
    document_id = task.get("document_id")
    if isinstance(document_id, str) and document_id:
        return document_id
    return None


def _result_payload(
    *,
    status: str,
    task_id: str | None = None,
    task: JSONDict | None = None,
    form_id: str | None = None,
    document_id: str | None = None,
    download_url: str | None = None,
    download_path: str | None = None,
) -> JSONDict:
    return {
        "status": status,
        "task_id": task_id,
        "task": task,
        "form_id": form_id,
        "document_id": document_id,
        "download_url": download_url,
        "download_path": download_path,
    }


def _is_transient_task_not_found(error: CliError) -> bool:
    details = error.details if isinstance(error.details, dict) else {}
    if details.get("status_code") != 404:
        return False

    response_payload = details.get("response")
    if isinstance(response_payload, dict):
        detail = response_payload.get("detail")
        if isinstance(detail, str) and detail.strip().lower() == "task not found":
            return True
    return False


def _extract_progress(task: JSONDict) -> int | None:
    value = task.get("progress")
    if isinstance(value, int):
        return value
    if isinstance(value, float):
        return int(value)
    return None


def _should_emit_task_update(
    *,
    status: str,
    progress: int | None,
    last_status: str | None,
    last_progress_reported: int | None,
    elapsed_seconds: float,
    last_emit: float,
) -> bool:
    if last_status is None:
        return True
    if status != last_status:
        return True
    if progress is not None and (
        last_progress_reported is None or progress >= last_progress_reported + 10 or progress == 100
    ):
        return True
    if elapsed_seconds - last_emit >= 15:
        return True
    return False
