"""
camcops_server/tasks/ceca.py

===============================================================================

    Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
    Created by Rudolf Cardinal (rnc1001@cam.ac.uk).

    This file is part of CamCOPS.

    CamCOPS is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    CamCOPS is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.

===============================================================================

"""

from typing import Any, Dict, List, Optional, Sequence

import cardinal_pythonlib.rnc_web as ws
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql.sqltypes import Boolean, Integer, UnicodeText

from camcops_server.cc_modules.cc_constants import CssClass
from camcops_server.cc_modules.cc_html import (
    answer,
    get_yes_no,
    get_yes_no_none,
    subheading_spanning_two_columns,
    tr,
    tr_qa,
)
from camcops_server.cc_modules.cc_request import CamcopsRequest
from camcops_server.cc_modules.cc_sqla_coltypes import (
    BIT_CHECKER,
    mapped_camcops_column,
    MIN_ZERO_CHECKER,
    ONE_TO_FOUR_CHECKER,
    ONE_TO_FIVE_CHECKER,
    PermittedValueChecker,
    ZERO_TO_TWO_CHECKER,
    ZERO_TO_THREE_CHECKER,
    ZERO_TO_FOUR_CHECKER,
    ZERO_TO_FIVE_CHECKER,
)
from camcops_server.cc_modules.cc_summaryelement import SummaryElement
from camcops_server.cc_modules.cc_task import (
    get_from_dict,
    Task,
    TaskHasPatientMixin,
)


# =============================================================================
# CECA-Q3
# =============================================================================

FREQUENCY_COMMENT = "Frequency (0 never - 3 often)"


class CecaQ3(TaskHasPatientMixin, Task):  # type: ignore[misc]
    """
    Server implementation of the CECA-Q3 task.
    """

    __tablename__ = "cecaq3"
    shortname = "CECA-Q3"

    # -------------------------------------------------------------------------
    # Section 1(A)
    # -------------------------------------------------------------------------
    s1a_motherfigure_birthmother: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment="Raised by, maternal, birth mother?",
        )
    )
    s1a_motherfigure_stepmother: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment="Raised by, maternal, stepmother?",
        )
    )
    s1a_motherfigure_femalerelative: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment="Raised by, maternal, female relative?",
        )
    )
    s1a_motherfigure_femalerelative_detail: Mapped[Optional[str]] = (
        mapped_column(
            UnicodeText,
            comment="Raised by, maternal, female relative, detail",
        )
    )
    s1a_motherfigure_familyfriend: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment="Raised by, maternal, family friend?",
        )
    )
    s1a_motherfigure_fostermother: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment="Raised by, maternal, foster mother?",
        )
    )
    s1a_motherfigure_adoptivemother: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment="Raised by, maternal, adoptive mother?",
        )
    )
    s1a_motherfigure_other: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Raised by, maternal, other?",
    )
    s1a_motherfigure_other_detail: Mapped[Optional[str]] = mapped_column(
        UnicodeText,
        comment="Raised by, maternal, other, detail",
    )
    s1a_fatherfigure_birthfather: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment="Raised by, paternal, birth father?",
        )
    )
    s1a_fatherfigure_stepfather: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment="Raised by, paternal, stepfather?",
        )
    )
    s1a_fatherfigure_malerelative: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment="Raised by, paternal, male relative?",
        )
    )
    s1a_fatherfigure_malerelative_detail: Mapped[Optional[str]] = (
        mapped_column(
            UnicodeText,
            comment="Raised by, paternal, male relative, detail",
        )
    )
    s1a_fatherfigure_familyfriend: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment="Raised by, paternal, family friend?",
        )
    )
    s1a_fatherfigure_fosterfather: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment="Raised by, paternal, foster father?",
        )
    )
    s1a_fatherfigure_adoptivefather: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment="Raised by, paternal, adoptive father?",
        )
    )
    s1a_fatherfigure_other: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Raised by, paternal, other?",
    )
    s1a_fatherfigure_other_detail: Mapped[Optional[str]] = mapped_column(
        UnicodeText,
        comment="Raised by, paternal, other, detail",
    )

    # -------------------------------------------------------------------------
    # Section 1(B)
    # -------------------------------------------------------------------------
    s1b_institution: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="In institution before 17?",
    )
    s1b_institution_time_years: Mapped[Optional[float]] = (
        mapped_camcops_column(
            permitted_value_checker=MIN_ZERO_CHECKER,
            comment="In institution, time (years)",
        )
    )

    # -------------------------------------------------------------------------
    # Section 1(C)
    # -------------------------------------------------------------------------
    s1c_mother_died: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Mother died before 17?",
    )
    s1c_father_died: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Father died before 17?",
    )
    s1c_mother_died_subject_aged: Mapped[Optional[float]] = (
        mapped_camcops_column(
            permitted_value_checker=MIN_ZERO_CHECKER,
            comment="Age when mother died (years)",
        )
    )
    s1c_father_died_subject_aged: Mapped[Optional[float]] = (
        mapped_camcops_column(
            permitted_value_checker=MIN_ZERO_CHECKER,
            comment="Age when father died (years)",
        )
    )
    s1c_separated_from_mother: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Separated from mother for >=1y before 17?",
    )
    s1c_separated_from_father: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Separated from father for >=1y before 17?",
    )
    s1c_first_separated_from_mother_aged: Mapped[Optional[float]] = (
        mapped_camcops_column(
            permitted_value_checker=MIN_ZERO_CHECKER,
            comment="Maternal separation, age (years)",
        )
    )
    s1c_first_separated_from_father_aged: Mapped[Optional[float]] = (
        mapped_camcops_column(
            permitted_value_checker=MIN_ZERO_CHECKER,
            comment="Paternal separation, age (years)",
        )
    )
    s1c_mother_how_long_first_separation_years: Mapped[Optional[float]] = (
        mapped_camcops_column(
            permitted_value_checker=MIN_ZERO_CHECKER,
            comment="Maternal separation, how long first separation (y)",
        )
    )
    s1c_father_how_long_first_separation_years: Mapped[Optional[float]] = (
        mapped_camcops_column(
            permitted_value_checker=MIN_ZERO_CHECKER,
            comment="Paternal separation, how long first separation (y)",
        )
    )
    s1c_mother_separation_reason: Mapped[Optional[int]] = (
        mapped_camcops_column(
            permitted_value_checker=PermittedValueChecker(
                minimum=1, maximum=6
            ),
            comment="Maternal separation, reason "
            "(1 illness, 2 work, 3 divorce/separation, 4 never knew, "
            "5 abandoned, 6 other)",
        )
    )
    s1c_father_separation_reason: Mapped[Optional[int]] = (
        mapped_camcops_column(
            permitted_value_checker=PermittedValueChecker(
                minimum=1, maximum=6
            ),
            comment="Paternal separation, reason "
            "(1 illness, 2 work, 3 divorce/separation, 4 never knew, "
            "5 abandoned, 6 other)",
        )
    )
    s1c_describe_experience: Mapped[Optional[str]] = mapped_column(
        UnicodeText,
        comment="Loss of/separation from parent, description",
    )

    # -------------------------------------------------------------------------
    # Section 2(A)
    # -------------------------------------------------------------------------
    s2a_which_mother_figure: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=PermittedValueChecker(minimum=0, maximum=5),
        comment="Mother figure, which one (0 none/skip, 1 birth mother, "
        "2 stepmother, 3 other relative, 4 other non-relative, "
        "5 other)",
    )
    s2a_which_mother_figure_other_detail: Mapped[Optional[str]] = (
        mapped_column(
            UnicodeText,
            comment="Mother figure, other, detail",
        )
    )
    s2a_q1: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Mother figure, difficult to please (1 no - 5 yes)",
    )
    s2a_q2: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Mother figure, concerned re my worries (1 no - 5 yes)",
    )
    s2a_q3: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Mother figure, interested re school (1 no - 5 yes)",
    )
    s2a_q4: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Mother figure, made me feel unwanted (1 no - 5 yes)",
    )
    s2a_q5: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Mother figure, better when upset (1 no - 5 yes)",
    )
    s2a_q6: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Mother figure, critical (1 no - 5 yes)",
    )
    s2a_q7: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Mother figure, unsupervised <10y (1 no - 5 yes)",
    )
    s2a_q8: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Mother figure, time to talk (1 no - 5 yes)",
    )
    s2a_q9: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Mother figure, nuisance (1 no - 5 yes)",
    )
    s2a_q10: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Mother figure, picked on unfairly (1 no - 5 yes)",
    )
    s2a_q11: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Mother figure, there if needed (1 no - 5 yes)",
    )
    s2a_q12: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Mother figure, interested in friends (1 no - 5 yes)",
    )
    s2a_q13: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Mother figure, concerned re whereabouts (1 no - 5 yes)",
    )
    s2a_q14: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Mother figure, cared when ill (1 no - 5 yes)",
    )
    s2a_q15: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Mother figure, neglected basic needs (1 no - 5 yes)",
    )
    s2a_q16: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Mother figure, preferred siblings (1 no - 5 yes)",
    )
    s2a_extra: Mapped[Optional[str]] = mapped_column(
        UnicodeText, comment="Mother figure, extra detail"
    )

    # -------------------------------------------------------------------------
    # Section 2(B)
    # -------------------------------------------------------------------------
    s2b_q1: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Mother figure, tease me (0 no - 2 yes)",
    )
    s2b_q2: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Mother figure, made me keep secrets (0 no - 2 yes)",
    )
    s2b_q3: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Mother figure, undermined confidence (0 no - 2 yes)",
    )
    s2b_q4: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Mother figure, contradictory (0 no - 2 yes)",
    )
    s2b_q5: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Mother figure, played on fears (0 no - 2 yes)",
    )
    s2b_q6: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Mother figure, liked to see me suffer (0 no - 2 yes)",
    )
    s2b_q7: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Mother figure, humiliated me (0 no - 2 yes)",
    )
    s2b_q8: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Mother figure, shamed me before others (0 no - 2 yes)",
    )
    s2b_q9: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Mother figure, rejecting (0 no - 2 yes)",
    )
    s2b_q10: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Mother figure, took things I cherished (0 no - 2 yes)",
    )
    s2b_q11: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Mother figure, eat disliked until sick (0 no - 2 yes)",
    )
    s2b_q12: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Mother figure, deprived light/food/company (0 no - 2 yes)",
    )
    s2b_q13: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Mother figure, wouldn't let me mix (0 no - 2 yes)",
    )
    s2b_q14: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Mother figure, obedience through guilt (0 no - 2 yes)",
    )
    s2b_q15: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Mother figure, threatened to hurt people dear to me "
        "(0 no - 2 yes)",
    )
    s2b_q16: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Mother figure, forced to break law (0 no - 2 yes)",
    )
    s2b_q17: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Mother figure, said wanted me dead (0 no - 2 yes)",
    )
    s2b_q1_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s2b_q2_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s2b_q3_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s2b_q4_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s2b_q5_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s2b_q6_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s2b_q7_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s2b_q8_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s2b_q9_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s2b_q10_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s2b_q11_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s2b_q12_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s2b_q13_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s2b_q14_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s2b_q15_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s2b_q16_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s2b_q17_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s2b_age_began: Mapped[Optional[float]] = mapped_camcops_column(
        permitted_value_checker=MIN_ZERO_CHECKER,
        comment="Age these experienced began (years)",
    )
    s2b_extra: Mapped[Optional[str]] = mapped_column(
        UnicodeText, comment="Extra detail"
    )

    # -------------------------------------------------------------------------
    # Section 3(A)
    # -------------------------------------------------------------------------
    s3a_which_father_figure: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_FIVE_CHECKER,
        comment="Father figure, which one (0 none/skip, 1 birth father, "
        "2 stepfather, 3 other relative, 4 other non-relative, "
        "5 other)",
    )
    s3a_which_father_figure_other_detail: Mapped[Optional[str]] = (
        mapped_column(
            UnicodeText,
            comment="Father figure, other, detail",
        )
    )
    s3a_q1: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Father figure, difficult to please (1 no - 5 yes)",
    )
    s3a_q2: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Father figure, concerned re my worries (1 no - 5 yes)",
    )
    s3a_q3: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Father figure, interested re school (1 no - 5 yes)",
    )
    s3a_q4: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Father figure, made me feel unwanted (1 no - 5 yes)",
    )
    s3a_q5: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Father figure, better when upset (1 no - 5 yes)",
    )
    s3a_q6: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Father figure, critical (1 no - 5 yes)",
    )
    s3a_q7: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Father figure, unsupervised <10y (1 no - 5 yes)",
    )
    s3a_q8: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Father figure, time to talk (1 no - 5 yes)",
    )
    s3a_q9: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Father figure, nuisance (1 no - 5 yes)",
    )
    s3a_q10: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Father figure, picked on unfairly (1 no - 5 yes)",
    )
    s3a_q11: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Father figure, there if needed (1 no - 5 yes)",
    )
    s3a_q12: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Father figure, interested in friends (1 no - 5 yes)",
    )
    s3a_q13: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Father figure, concerned re whereabouts (1 no - 5 yes)",
    )
    s3a_q14: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Father figure, cared when ill (1 no - 5 yes)",
    )
    s3a_q15: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Father figure, neglected basic needs (1 no - 5 yes)",
    )
    s3a_q16: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Father figure, preferred siblings (1 no - 5 yes)",
    )
    s3a_extra: Mapped[Optional[str]] = mapped_column(
        UnicodeText, comment="Father figure, extra detail"
    )

    # -------------------------------------------------------------------------
    # Section 3(B)
    # -------------------------------------------------------------------------
    s3b_q1: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Father figure, tease me (0 no - 2 yes)",
    )
    s3b_q2: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Father figure, made me keep secrets (0 no - 2 yes)",
    )
    s3b_q3: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Father figure, undermined confidence (0 no - 2 yes)",
    )
    s3b_q4: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Father figure, contradictory (0 no - 2 yes)",
    )
    s3b_q5: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Father figure, played on fears (0 no - 2 yes)",
    )
    s3b_q6: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Father figure, liked to see me suffer (0 no - 2 yes)",
    )
    s3b_q7: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Father figure, humiliated me (0 no - 2 yes)",
    )
    s3b_q8: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Father figure, shamed me before others (0 no - 2 yes)",
    )
    s3b_q9: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Father figure, rejecting (0 no - 2 yes)",
    )
    s3b_q10: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Father figure, took things I cherished (0 no - 2 yes)",
    )
    s3b_q11: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Father figure, eat disliked until sick (0 no - 2 yes)",
    )
    s3b_q12: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Father figure, deprived light/food/company (0 no - 2 yes)",
    )
    s3b_q13: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Father figure, wouldn't let me mix (0 no - 2 yes)",
    )
    s3b_q14: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Father figure, obedience through guilt (0 no - 2 yes)",
    )
    s3b_q15: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Father figure, threatened to hurt people dear to me "
        "(0 no - 2 yes)",
    )
    s3b_q16: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Father figure, forced to break law (0 no - 2 yes)",
    )
    s3b_q17: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Father figure, said wanted me dead (0 no - 2 yes)",
    )
    s3b_q1_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s3b_q2_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s3b_q3_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s3b_q4_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s3b_q5_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s3b_q6_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s3b_q7_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s3b_q8_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s3b_q9_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s3b_q10_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s3b_q11_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s3b_q12_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s3b_q13_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s3b_q14_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s3b_q15_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s3b_q16_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s3b_q17_frequency: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment=FREQUENCY_COMMENT,
    )
    s3b_age_began: Mapped[Optional[float]] = mapped_camcops_column(
        permitted_value_checker=MIN_ZERO_CHECKER,
        comment="Age these experienced began (years)",
    )
    s3b_extra: Mapped[Optional[str]] = mapped_column(
        UnicodeText, comment="Extra detail"
    )

    # -------------------------------------------------------------------------
    # Section 3(C)
    # -------------------------------------------------------------------------
    s3c_q1: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Responsibility (1 no - 5 yes)",
    )
    s3c_q2: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Housework (1 no - 5 yes)",
    )
    s3c_q3: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Look after young siblings (1 no - 5 yes)",
    )
    s3c_q4: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Cooking/cleaning (1 no - 5 yes)",
    )
    s3c_q5: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Miss school for domestic responsibilities (1 no - 5 yes)",
    )
    s3c_q6: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Miss seeing friends for domestic responsibilities "
        "(1 no - 5 yes)",
    )
    s3c_q7: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Parents said they couldn't cope (1 no - 5 yes)",
    )
    s3c_q8: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Parents looked to you for help (1 no - 5 yes)",
    )
    s3c_q9: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Parents coped if you were hurt/ill (1 no - 5 yes)",
    )
    s3c_q10: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Parents confided their problems (1 no - 5 yes)",
    )
    s3c_q11: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Parents relied on you for emotional support (1 no - 5 yes)",
    )
    s3c_q12: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Parents cried in front of you (1 no - 5 yes)",
    )
    s3c_q13: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Concerned/worried re parent (1 no - 5 yes)",
    )
    s3c_q14: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Tried to support/care for parent (1 no - 5 yes)",
    )
    s3c_q15: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Try to make parent smile when upset (1 no - 5 yes)",
    )
    s3c_q16: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Parents made you feel guilty for their sacrifices "
        "(1 no - 5 yes)",
    )
    s3c_q17: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Had to keep secrets for parent (1 no - 5 yes)",
    )
    s3c_which_parent_cared_for: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_FOUR_CHECKER,
        comment="Which parent did you have to provide care for (0 none, "
        "1 mother, 2 father, 3 both, 4 other)",
    )
    s3c_parent_mental_problem: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Parent/s had emotional/mental health problems (0 no - 2 yes)",
    )
    s3c_parent_physical_problem: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Parent/s had disability/physical illness (0 no - 2 yes)",
    )

    # -------------------------------------------------------------------------
    # Section 4(A)
    # -------------------------------------------------------------------------
    s4a_adultconfidant: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Adult confidant?",
    )
    s4a_adultconfidant_mother: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Adult confidant, mother?",
    )
    s4a_adultconfidant_father: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Adult confidant, father?",
    )
    s4a_adultconfidant_otherrelative: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment="Adult confidant, other relative?",
        )
    )
    s4a_adultconfidant_familyfriend: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment="Adult confidant, family friend?",
        )
    )
    s4a_adultconfidant_responsibleadult: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment="Adult confidant, teacher/vicar/etc.?",
        )
    )
    s4a_adultconfidant_other: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Adult confidant, other?",
    )
    s4a_adultconfidant_other_detail: Mapped[Optional[str]] = mapped_column(
        UnicodeText,
        comment="Adult confidant, other, detail",
    )
    s4a_adultconfidant_additional: Mapped[Optional[str]] = mapped_column(
        UnicodeText,
        comment="Adult confidant, additional notes",
    )

    # -------------------------------------------------------------------------
    # Section 4(B)
    # -------------------------------------------------------------------------
    s4b_childconfidant: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Child confidant?",
    )
    s4b_childconfidant_sister: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Child confidant, sister?",
    )
    s4b_childconfidant_brother: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Child confidant, brother?",
    )
    s4b_childconfidant_otherrelative: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment="Child confidant, other relative?",
        )
    )
    s4b_childconfidant_closefriend: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment="Child confidant, close friend?",
        )
    )
    s4b_childconfidant_otherfriend: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment="Child confidant, other less close friend(s)?",
        )
    )
    s4b_childconfidant_other: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Child confidant, other person?",
    )
    s4b_childconfidant_other_detail: Mapped[Optional[str]] = mapped_column(
        UnicodeText,
        comment="Child confidant, other person, detail",
    )
    s4b_childconfidant_additional: Mapped[Optional[str]] = mapped_column(
        UnicodeText,
        comment="Child confidant, additional notes",
    )

    # -------------------------------------------------------------------------
    # Section 4(C)
    # -------------------------------------------------------------------------
    s4c_closest_mother: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Two closest people include: mother?",
    )
    s4c_closest_father: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Two closest people include: father?",
    )
    s4c_closest_sibling: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Two closest people include: sibling?",
    )
    s4c_closest_otherrelative: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Two closest people include: other relative?",
    )
    s4c_closest_adultfriend: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Two closest people include: adult family friend?",
    )
    s4c_closest_childfriend: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Two closest people include: friend your age?",
    )
    s4c_closest_other: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Two closest people include: other?",
    )
    s4c_closest_other_detail: Mapped[Optional[str]] = mapped_column(
        UnicodeText,
        comment="Two closest people include: other, detail",
    )
    s4c_closest_additional: Mapped[Optional[str]] = mapped_column(
        UnicodeText,
        comment="Two closest people include: additional notes",
    )

    # -------------------------------------------------------------------------
    # Section 5(C)
    # -------------------------------------------------------------------------
    s5c_physicalabuse: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Physical abuse?",
    )
    s5c_abused_by_mother: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Physical abuse, by mother?",
    )
    s5c_abused_by_father: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Physical abuse, by father?",
    )
    s5c_mother_abuse_age_began: Mapped[Optional[float]] = (
        mapped_camcops_column(
            comment="Physical abuse, by mother, age began (y)",
        )
    )
    s5c_father_abuse_age_began: Mapped[Optional[float]] = (
        mapped_camcops_column(
            comment="Physical abuse, by father, age began (y)",
        )
    )
    s5c_mother_hit_more_than_once: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment="Physical abuse, by mother, hit on >1 occasion",
        )
    )
    s5c_father_hit_more_than_once: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment="Physical abuse, by father, hit on >1 occasion",
        )
    )
    s5c_mother_hit_how: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FOUR_CHECKER,
        comment="Physical abuse, by mother, hit how (1 belt/stick, "
        "2 punched/kicked, 3 hit with hand, 4 other)",
    )
    s5c_father_hit_how: Mapped[Optional[int]] = mapped_camcops_column(
        permitted_value_checker=ONE_TO_FOUR_CHECKER,
        comment="Physical abuse, by father, hit how (1 belt/stick, "
        "2 punched/kicked, 3 hit with hand, 4 other)",
    )
    s5c_mother_injured: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Physical abuse, by mother, injured?",
    )
    s5c_father_injured: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Physical abuse, by father, injured?",
    )
    s5c_mother_out_of_control: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Physical abuse, by mother, out of control?",
    )
    s5c_father_out_of_control: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Physical abuse, by father, out of control?",
    )
    s5c_parental_abuse_description: Mapped[Optional[str]] = mapped_column(
        UnicodeText,
        comment="Physical abuse, description",
    )
    s5c_abuse_by_nonparent: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Physical abuse, by anyone else in household?",
    )
    s5c_nonparent_abuse_description: Mapped[Optional[str]] = mapped_column(
        UnicodeText,
        comment="Physical abuse, nonparent, description",
    )

    # -------------------------------------------------------------------------
    # Section 6
    # -------------------------------------------------------------------------
    s6_any_unwanted_sexual_experience: Mapped[Optional[int]] = (
        mapped_camcops_column(
            # Prior to 2.1.0: was cctype="BOOL" on the server, but this gave
            # TINYINT(1), which can store -128 to 128. Corrected to Integer.
            "s6_any_unwanted_sexual_experience",
            Integer,
            permitted_value_checker=ZERO_TO_TWO_CHECKER,
            comment="Any unwanted sexual experiences (0 no - 2 yes)",
        )
    )
    s6_unwanted_intercourse: Mapped[Optional[int]] = mapped_camcops_column(
        # Prior to 2.1.0: was cctype="BOOL" on the server, but this gave
        # TINYINT(1), which can store -128 to 128. Corrected to Integer.
        permitted_value_checker=ZERO_TO_TWO_CHECKER,
        comment="Unwanted intercourse before 17yo (0 no - 2 yes)",
    )
    s6_upsetting_sexual_adult_authority: Mapped[Optional[int]] = (
        mapped_camcops_column(
            # Prior to 2.1.0: was cctype="BOOL" on the server, but this gave
            # TINYINT(1), which can store -128 to 128. Corrected to Integer.
            permitted_value_checker=ZERO_TO_TWO_CHECKER,
            comment="Upsetting sexual experiences under 17yo with "
            "related adult or someone in authority (0 no - 2 yes)",
        )
    )
    s6_first_age: Mapped[Optional[float]] = mapped_camcops_column(
        permitted_value_checker=MIN_ZERO_CHECKER,
        comment="Sexual abuse, first experience, age it began",
    )
    s6_other_age: Mapped[Optional[float]] = mapped_camcops_column(
        permitted_value_checker=MIN_ZERO_CHECKER,
        comment="Sexual abuse, other experience, age it began",
    )
    s6_first_person_known: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Sexual abuse, first experience, knew the person?",
    )
    s6_other_person_known: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Sexual abuse, other experience, knew the person?",
    )
    s6_first_relative: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Sexual abuse, first experience, person was a relative?",
    )
    s6_other_relative: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Sexual abuse, other experience, person was a relative?",
    )
    s6_first_in_household: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Sexual abuse, first experience, person lived in household?",
    )
    s6_other_in_household: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Sexual abuse, other experience, person lived in household?",
    )
    s6_first_more_than_once: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Sexual abuse, first experience, happened more than once?",
    )
    s6_other_more_than_once: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Sexual abuse, other experience, happened more than once?",
    )
    s6_first_touch_privates_subject: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment=(
                "Sexual abuse, first experience, touched your private parts?"
            ),
        )
    )
    s6_other_touch_privates_subject: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment=(
                "Sexual abuse, other experience, touched your private parts?"
            ),
        )
    )
    s6_first_touch_privates_other: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment=(
                "Sexual abuse, first experience, touched their private parts?"
            ),
        )
    )
    s6_other_touch_privates_other: Mapped[Optional[bool]] = (
        mapped_camcops_column(
            permitted_value_checker=BIT_CHECKER,
            comment=(
                "Sexual abuse, other experience, touched their private parts?"
            ),
        )
    )
    s6_first_intercourse: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Sexual abuse, first experience, sexual intercourse?",
    )
    s6_other_intercourse: Mapped[Optional[bool]] = mapped_camcops_column(
        permitted_value_checker=BIT_CHECKER,
        comment="Sexual abuse, other experience, sexual intercourse?",
    )
    s6_unwanted_sexual_description: Mapped[Optional[str]] = mapped_column(
        UnicodeText,
        comment="Sexual abuse, description",
    )

    # -------------------------------------------------------------------------
    # Final
    # -------------------------------------------------------------------------
    any_other_comments: Mapped[Optional[str]] = mapped_camcops_column(
        UnicodeText, comment="Any other comments"
    )

    @staticmethod
    def longname(req: "CamcopsRequest") -> str:
        _ = req.gettext
        return _("Childhood Experience of Care and Abuse Questionnaire")

    def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
        return self.standard_task_summary_fields() + [
            SummaryElement(
                name="parental_loss_risk",
                coltype=Boolean(),
                value=self.parental_loss_risk(),
                comment="Parental loss risk factor?",
            ),
            SummaryElement(
                name="parental_loss_high_risk",
                coltype=Boolean(),
                value=self.parental_loss_high_risk(),
                comment="Parental loss higher risk factor?",
            ),
            SummaryElement(
                name="mother_antipathy",
                coltype=Integer(),
                value=self.mother_antipathy(),
                comment="Maternal antipathy score (8-40)",
            ),
            SummaryElement(
                name="mother_neglect",
                coltype=Integer(),
                value=self.mother_neglect(),
                comment="Maternal neglect score (8-40)",
            ),
            SummaryElement(
                name="mother_psychological_abuse",
                coltype=Integer(),
                value=self.mother_psychological_abuse(),
                comment="Maternal psychological abuse score (0-85)",
            ),
            SummaryElement(
                name="father_antipathy",
                coltype=Integer(),
                value=self.father_antipathy(),
                comment="Paternal antipathy score (8-40)",
            ),
            SummaryElement(
                name="father_neglect",
                coltype=Integer(),
                value=self.father_neglect(),
                comment="Paternal neglect score (8-40)",
            ),
            SummaryElement(
                name="father_psychological_abuse",
                coltype=Integer(),
                value=self.father_psychological_abuse(),
                comment="Paternal psychological abuse score (0-85)",
            ),
            SummaryElement(
                name="role_reversal",
                coltype=Integer(),
                value=self.role_reversal(),
                comment="Role reversal score (17-85)",
            ),
            SummaryElement(
                name="physical_abuse_screen",
                coltype=Integer(),
                value=self.physical_abuse_screen(),
                comment="Physical abuse screen (0-1)",
            ),
            SummaryElement(
                name="physical_abuse_severity_mother",
                coltype=Integer(),
                value=self.physical_abuse_severity_mother(),
                comment="Maternal physical abuse severity score (0-4)",
            ),
            SummaryElement(
                name="physical_abuse_severity_father",
                coltype=Integer(),
                value=self.physical_abuse_severity_father(),
                comment="Paternal physical abuse severity score (0-4)",
            ),
            SummaryElement(
                name="sexual_abuse_screen",
                coltype=Integer(),
                value=self.sexual_abuse_screen(),
                comment="Sexual abuse screen (0-3)",
            ),
            SummaryElement(
                name="sexual_abuse_score_first",
                coltype=Integer(),
                value=self.sexual_abuse_score_first(),
                comment="First sexual abuse severity score (0-7)",
            ),
            SummaryElement(
                name="sexual_abuse_score_other",
                coltype=Integer(),
                value=self.sexual_abuse_score_other(),
                comment="Other sexual abuse severity score (0-7)",
            ),
        ]

    # -------------------------------------------------------------------------
    # Complete?
    # -------------------------------------------------------------------------

    def is_complete(self) -> bool:
        return (
            self.complete_1a()
            and self.complete_1b()
            and self.complete_1c()
            and self.complete_2a()
            and self.complete_2b()
            and self.complete_3a()
            and self.complete_3b()
            and self.complete_3c()
            and self.complete_4a()
            and self.complete_4b()
            and self.complete_4c()
            and self.complete_5()
            and self.complete_6()
            and self.field_contents_valid()
        )

    def is_at_least_one_field_true(self, fields: List[str]) -> bool:
        for f in fields:
            if getattr(self, f):
                return True
        return True

    def complete_1a(self) -> bool:
        if not self.is_at_least_one_field_true(
            [
                "s1a_motherfigure_birthmother",
                "s1a_motherfigure_stepmother",
                "s1a_motherfigure_femalerelative",
                "s1a_motherfigure_familyfriend",
                "s1a_motherfigure_fostermother",
                "s1a_motherfigure_adoptivemother",
                "s1a_motherfigure_other",
                "s1a_fatherfigure_birthfather",
                "s1a_fatherfigure_stepfather",
                "s1a_fatherfigure_malerelative",
                "s1a_fatherfigure_familyfriend",
                "s1a_fatherfigure_fosterfather",
                "s1a_fatherfigure_adoptivefather",
                "s1a_fatherfigure_other",
            ]
        ):
            return False
        if (
            self.s1a_motherfigure_other
            and not self.s1a_motherfigure_other_detail
        ):
            return False
        if (
            self.s1a_motherfigure_femalerelative
            and not self.s1a_motherfigure_femalerelative_detail
        ):
            return False
        if (
            self.s1a_fatherfigure_other
            and not self.s1a_fatherfigure_other_detail
        ):
            return False
        if (
            self.s1a_fatherfigure_malerelative
            and not self.s1a_fatherfigure_malerelative_detail
        ):
            return False
        return True

    def complete_1b(self) -> bool:
        if self.s1b_institution is None:
            return False
        if self.s1b_institution and self.s1b_institution_time_years is None:
            return False
        return True

    def complete_1c(self) -> bool:
        if self.s1c_mother_died is None or self.s1c_father_died is None:
            return False
        if self.s1c_mother_died and self.s1c_mother_died_subject_aged is None:
            return False
        if self.s1c_father_died and self.s1c_father_died_subject_aged is None:
            return False
        if (
            self.s1c_separated_from_mother is None
            or self.s1c_separated_from_father is None
        ):
            return False
        if self.s1c_separated_from_mother:
            if self.any_fields_none(
                [
                    "s1c_first_separated_from_mother_aged",
                    "s1c_mother_how_long_first_separation_years",
                    "s1c_mother_separation_reason",
                ]
            ):
                return False
        if self.s1c_separated_from_father:
            if self.any_fields_none(
                [
                    "s1c_first_separated_from_father_aged",
                    "s1c_father_how_long_first_separation_years",
                    "s1c_father_separation_reason",
                ]
            ):
                return False
        return True

    def complete_2a(self) -> bool:
        if self.s2a_which_mother_figure is None:
            return False
        if self.s2a_which_mother_figure == 0:
            return True
        if (
            self.s2a_which_mother_figure == 5
            and self.s2a_which_mother_figure_other_detail is None
        ):
            return False
        for i in range(1, 16):  # not q16 (siblings)
            if getattr(self, "s2a_q" + str(i)) is None:
                return False
        return True

    def complete_2b(self) -> bool:
        abuse = False
        if self.s2a_which_mother_figure == 0:
            return True
        for i in range(1, 18):
            if getattr(self, "s2b_q" + str(i)) is None:
                return False
            if getattr(self, "s2b_q" + str(i)) != 0:
                abuse = True
                if getattr(self, "s2b_q" + str(i) + "_frequency") is None:
                    return False
        if abuse and self.s2b_age_began is None:
            return False
        return True

    def complete_3a(self) -> bool:
        if self.s3a_which_father_figure is None:
            return False
        if self.s3a_which_father_figure == 0:
            return True
        if (
            self.s3a_which_father_figure == 5
            and self.s3a_which_father_figure_other_detail is None
        ):
            return False
        for i in range(1, 16):  # not q16 (siblings)
            if getattr(self, "s3a_q" + str(i)) is None:
                return False
        return True

    def complete_3b(self) -> bool:
        abuse = False
        if self.s3a_which_father_figure == 0:
            return True
        for i in range(1, 18):
            if getattr(self, "s3b_q" + str(i)) is None:
                return False
            if getattr(self, "s3b_q" + str(i)) != 0:
                abuse = True
                if getattr(self, "s3b_q" + str(i) + "_frequency") is None:
                    return False
        if abuse and self.s3b_age_began is None:
            return False
        return True

    def complete_3c(self) -> bool:
        return self.all_fields_not_none(
            [
                "s3c_q1",
                "s3c_q2",
                "s3c_q3",
                "s3c_q4",
                "s3c_q5",
                "s3c_q6",
                "s3c_q7",
                "s3c_q8",
                "s3c_q9",
                "s3c_q10",
                "s3c_q11",
                "s3c_q12",
                "s3c_q13",
                "s3c_q14",
                "s3c_q15",
                "s3c_q16",
                "s3c_q17",
                "s3c_which_parent_cared_for",
                "s3c_parent_mental_problem",
                "s3c_parent_physical_problem",
            ]
        )

    def complete_4a(self) -> bool:
        if self.s4a_adultconfidant is None:
            return False
        if not self.s4a_adultconfidant:
            return True
        if not self.is_at_least_one_field_true(
            [
                "s4a_adultconfidant_mother",
                "s4a_adultconfidant_father",
                "s4a_adultconfidant_otherrelative",
                "s4a_adultconfidant_familyfriend",
                "s4a_adultconfidant_responsibleadult",
                "s4a_adultconfidant_other",
            ]
        ):
            return False
        if (
            self.s4a_adultconfidant_other
            and not self.s4a_adultconfidant_other_detail
        ):
            return False
        return True

    def complete_4b(self) -> bool:
        if self.s4b_childconfidant is None:
            return False
        if not self.s4b_childconfidant:
            return True
        if not self.is_at_least_one_field_true(
            [
                "s4b_childconfidant_sister",
                "s4b_childconfidant_brother",
                "s4b_childconfidant_otherrelative",
                "s4b_childconfidant_closefriend",
                "s4b_childconfidant_otherfriend",
                "s4b_childconfidant_other",
            ]
        ):
            return False
        if (
            self.s4b_childconfidant_other
            and not self.s4b_childconfidant_other_detail
        ):
            return False
        return True

    def complete_4c(self) -> bool:
        n = 0
        if self.s4c_closest_mother:
            n += 1
        if self.s4c_closest_father:
            n += 1
        if self.s4c_closest_sibling:
            n += 1
        if self.s4c_closest_otherrelative:
            n += 1
        if self.s4c_closest_adultfriend:
            n += 1
        if self.s4c_closest_childfriend:
            n += 1
        if self.s4c_closest_other:
            n += 1
        if n < 2:
            return False
        if self.s4c_closest_other and not self.s4c_closest_other_detail:
            return False
        return True

    def complete_5(self) -> bool:
        if self.s5c_physicalabuse is None:
            return False
        if self.s5c_physicalabuse == 0:
            return True
        if (
            self.s5c_abused_by_mother is None
            or self.s5c_abused_by_father is None
            or self.s5c_abuse_by_nonparent is None
        ):
            return False
        if self.s5c_abused_by_mother:
            if self.any_fields_none(
                [
                    "s5c_mother_abuse_age_began",
                    "s5c_mother_hit_more_than_once",
                    "s5c_mother_hit_how",
                    "s5c_mother_injured",
                    "s5c_mother_out_of_control",
                ]
            ):
                return False
        if self.s5c_abused_by_father:
            if self.any_fields_none(
                [
                    "s5c_father_abuse_age_began",
                    "s5c_father_hit_more_than_once",
                    "s5c_father_hit_how",
                    "s5c_father_injured",
                    "s5c_father_out_of_control",
                ]
            ):
                return False
        if (
            self.s5c_abuse_by_nonparent
            and not self.s5c_nonparent_abuse_description
        ):
            return False
        return True

    def complete_6(self) -> bool:
        if (
            self.s6_any_unwanted_sexual_experience is None
            or self.s6_unwanted_intercourse is None
            or self.s6_upsetting_sexual_adult_authority is None
        ):
            return False
        if (
            self.s6_any_unwanted_sexual_experience == 0
            and self.s6_unwanted_intercourse == 0
            and self.s6_upsetting_sexual_adult_authority == 0
        ):
            return True
        if self.any_fields_none(
            [
                "s6_first_age",
                "s6_first_person_known",
                "s6_first_relative",
                "s6_first_in_household",
                "s6_first_more_than_once",
                "s6_first_touch_privates_subject",
                "s6_first_touch_privates_other",
                "s6_first_intercourse",
            ]
        ):
            return False
        # no checks for "other experience"
        return True

    # -------------------------------------------------------------------------
    # Scoring
    # -------------------------------------------------------------------------

    def total_sum_abort_if_none(self, fields: List[str]) -> Optional[int]:
        total = 0
        for field in fields:
            value = getattr(self, field)
            if value is None:
                return None
            total += value
        return total

    def total_nonzero_scores_1_abort_if_none(
        self, fields: Sequence[str]
    ) -> Optional[int]:
        total = 0
        for field in fields:
            value = getattr(self, field)
            if value is None:
                return None
            if value:
                total += 1
        return total

    def parental_loss_risk(self) -> bool:
        return bool(
            self.s1c_mother_died
            or self.s1c_father_died
            or self.s1c_separated_from_mother
            or self.s1c_separated_from_father
        )

    def parental_loss_high_risk(self) -> bool:
        return bool(
            self.s1c_separated_from_mother
            and (
                self.s1c_mother_separation_reason == 5
                or self.s1c_mother_separation_reason == 6
            )
            or self.s1c_separated_from_father
            and (
                self.s1c_father_separation_reason == 5
                or self.s1c_father_separation_reason == 6
            )
        )

    def mother_antipathy(self) -> Optional[int]:
        if self.s2a_which_mother_figure == 0:
            return None
        total = 0
        for i in (1, 4, 6, 8, 9, 10, 11, 16):
            score = getattr(self, "s2a_q" + str(i))
            if i == 16 and score is None:
                # Q16 is allowed to be blank (if no siblings)
                score = 0
            if score is None:
                return None
            if i in (8, 11):
                score = 6 - score  # reverse
            total += score
        return total

    def father_antipathy(self) -> Optional[int]:
        if self.s3a_which_father_figure == 0:
            return None
        total = 0
        for i in (1, 4, 6, 8, 9, 10, 11, 16):
            score = getattr(self, "s3a_q" + str(i))
            if i == 16 and score is None:
                # Q16 is allowed to be blank (if no siblings)
                score = 0
            if score is None:
                return None
            if i in (8, 11):
                score = 6 - score  # reverse
            total += score
        return total

    def mother_neglect(self) -> Optional[int]:
        if self.s2a_which_mother_figure == 0:
            return None
        total = 0
        for i in (2, 3, 5, 7, 12, 13, 14, 15):
            score = getattr(self, "s2a_q" + str(i))
            if score is None:
                return None
            if i in (2, 3, 5, 12, 13, 14):
                score = 6 - score  # reverse
            total += score
        return total

    def father_neglect(self) -> Optional[int]:
        if self.s3a_which_father_figure == 0:
            return None
        total = 0
        for i in (2, 3, 5, 7, 12, 13, 14, 15):
            score = getattr(self, "s3a_q" + str(i))
            if score is None:
                return None
            if i in (2, 3, 5, 12, 13, 14):
                score = 6 - score  # reverse
            total += score
        return total

    def mother_psychological_abuse(self) -> Optional[int]:
        if self.s2a_which_mother_figure == 0:
            return None
        total = 0
        for i in range(1, 18):
            score = getattr(self, "s2b_q" + str(i))
            if score is None:
                return None
            total += score
            freqscore = getattr(self, "s2b_q" + str(i) + "_frequency")
            if score != 0 and freqscore is None:
                return None
            if freqscore is not None:
                total += freqscore
        return total

    def father_psychological_abuse(self) -> Optional[int]:
        if self.s3a_which_father_figure == 0:
            return None
        total = 0
        for i in range(1, 18):
            score = getattr(self, "s3b_q" + str(i))
            if score is None:
                return None
            total += score
            freqscore = getattr(self, "s3b_q" + str(i) + "_frequency")
            if score != 0 and freqscore is None:
                return None
            if freqscore is not None:
                total += freqscore
        return total

    def role_reversal(self) -> Optional[int]:
        total = 0
        for i in range(1, 18):
            score = getattr(self, "s3c_q" + str(i))
            if score is None:
                return None
            total += score
        return total

    def physical_abuse_screen(self) -> Optional[int]:
        fields = (
            "s5c_physicalabuse",
        )  # tuple of length 1 -- requires the comma
        return self.total_nonzero_scores_1_abort_if_none(fields)

    def physical_abuse_severity_mother(self) -> Optional[int]:
        if self.physical_abuse_screen() == 0:
            return 0
        if self.s5c_abused_by_mother == 0:
            return 0
        mainfields = (
            "s5c_mother_hit_more_than_once",
            "s5c_mother_injured",
            "s5c_mother_out_of_control",
        )
        total = self.total_nonzero_scores_1_abort_if_none(mainfields)
        if total is None:
            return None
        if self.s5c_mother_hit_how is None:
            return None
        if self.s5c_mother_hit_how == 1 or self.s5c_mother_hit_how == 2:
            total += 1
        return total

    def physical_abuse_severity_father(self) -> Optional[int]:
        if self.physical_abuse_screen() == 0:
            return 0
        if self.s5c_abused_by_father == 0:
            return 0
        mainfields = (
            "s5c_father_hit_more_than_once",
            "s5c_father_injured",
            "s5c_father_out_of_control",
        )
        total = self.total_nonzero_scores_1_abort_if_none(mainfields)
        if total is None:
            return None
        if self.s5c_father_hit_how is None:
            return None
        if self.s5c_father_hit_how == 1 or self.s5c_father_hit_how == 2:
            total += 1
        return total

    def sexual_abuse_screen(self) -> Optional[int]:
        fields = (
            "s6_any_unwanted_sexual_experience",
            "s6_unwanted_intercourse",
            "s6_upsetting_sexual_adult_authority",
        )
        return self.total_nonzero_scores_1_abort_if_none(fields)

    def sexual_abuse_score_first(self) -> Optional[int]:
        if self.sexual_abuse_screen() == 0:
            return 0
        fields = (
            "s6_first_person_known",
            "s6_first_relative",
            "s6_first_in_household",
            "s6_first_more_than_once",
            "s6_first_touch_privates_subject",
            "s6_first_touch_privates_other",
            "s6_first_intercourse",
        )
        return self.total_nonzero_scores_1_abort_if_none(fields)

    def sexual_abuse_score_other(self) -> Optional[int]:
        if self.sexual_abuse_screen() == 0:
            return 0
        fields = (
            "s6_other_person_known",
            "s6_other_relative",
            "s6_other_in_household",
            "s6_other_more_than_once",
            "s6_other_touch_privates_subject",
            "s6_other_touch_privates_other",
            "s6_other_intercourse",
        )
        return self.total_nonzero_scores_1_abort_if_none(fields)

    # -------------------------------------------------------------------------
    # HTML
    # -------------------------------------------------------------------------

    def get_task_html(self, req: CamcopsRequest) -> str:
        def wxstring(wstringname: str) -> str:
            return self.wxstring(req, wstringname)

        def subheading_from_wstring(wstringname: str) -> str:
            return subheading_from_string(self.wxstring(req, wstringname))

        def subsubheading_from_wstring(wstringname: str) -> str:
            return subsubheading_from_string(self.wxstring(req, wstringname))

        def wstring_boolean(wstring: str, value: Any) -> str:
            return string_boolean_(req, self.wxstring(req, wstring), value)

        def wstring_numeric(wstring: str, value: Any) -> str:
            return string_numeric(self.wxstring(req, wstring), value)

        def wstring_string(wstring: str, value: str) -> str:
            return string_string(self.wxstring(req, wstring), value)

        def wstring_dict(wstring: str, value: Any, d: Dict) -> str:
            return string_dict(self.wxstring(req, wstring), value, d)

        def string_boolean(string: str, value: Any) -> str:
            return string_boolean_(req, string, value)

        separation_map = {
            None: None,
            1: "1 — " + wxstring("1c_separation_reason1"),
            2: "2 — " + wxstring("1c_separation_reason2"),
            3: "3 — " + wxstring("1c_separation_reason3"),
            4: "4 — " + wxstring("1c_separation_reason4"),
            5: "5 — " + wxstring("1c_separation_reason5"),
            6: "6 — " + wxstring("1c_separation_reason6"),
        }
        motherfigure_map = {
            None: None,
            0: "0 — " + wxstring("2a_which_option0"),
            1: "1 — " + wxstring("2a_which_option1"),
            2: "2 — " + wxstring("2a_which_option2"),
            3: "3 — " + wxstring("2a_which_option3"),
            4: "4 — " + wxstring("2a_which_option4"),
            5: "5 — " + wxstring("2a_which_option5"),
        }
        fatherfigure_map = {
            None: None,
            0: "0 — " + wxstring("3a_which_option0"),
            1: "1 — " + wxstring("3a_which_option1"),
            2: "2 — " + wxstring("3a_which_option2"),
            3: "3 — " + wxstring("3a_which_option3"),
            4: "4 — " + wxstring("3a_which_option4"),
            5: "5 — " + wxstring("3a_which_option5"),
        }
        no_yes_5way_map = {
            None: None,
            1: "1 — " + wxstring("options5way_notoyes_1"),
            2: "2 — (between not-at-all and unsure)",
            3: "3 — " + wxstring("options5way_notoyes_3"),
            4: "4 — (between unsure and yes-definitely)",
            5: "5 — " + wxstring("options5way_notoyes_5"),
        }
        no_yes_3way_map = {
            None: None,
            0: "0 — " + wxstring("options3way_noto_yes_0"),
            1: "1 — " + wxstring("options3way_noto_yes_1"),
            2: "2 — " + wxstring("options3way_noto_yes_2"),
        }
        frequency_map = {
            None: None,
            0: "0 — " + wxstring("optionsfrequency0"),
            1: "1 — " + wxstring("optionsfrequency1"),
            2: "2 — " + wxstring("optionsfrequency2"),
            3: "3 — " + wxstring("optionsfrequency3"),
        }
        parent_cared_for_map = {
            None: None,
            0: "0 — " + wxstring("3c_whichparentcaredfor_option0"),
            1: "1 — " + wxstring("3c_whichparentcaredfor_option1"),
            2: "2 — " + wxstring("3c_whichparentcaredfor_option2"),
            3: "3 — " + wxstring("3c_whichparentcaredfor_option3"),
            4: "4 — " + wxstring("3c_whichparentcaredfor_option4"),
        }
        hitting_map = {
            None: None,
            1: "1 — " + wxstring("5_hit_option_1"),
            2: "2 — " + wxstring("5_hit_option_2"),
            3: "3 — " + wxstring("5_hit_option_3"),
            4: "4 — " + wxstring("5_hit_option_4"),
        }
        html = (
            f"""
                <div class="{CssClass.SUMMARY}">
                    <table class="{CssClass.SUMMARY}">
            """
            + self.get_is_complete_tr(req)
            + tr_qa(
                "Parental loss risk factor? <sup>[1]</sup>",
                get_yes_no(req, self.parental_loss_risk()),
            )
            + tr_qa(
                "Parental loss higher risk factor? <sup>[2]</sup>",
                get_yes_no(req, self.parental_loss_high_risk()),
            )
            + tr_qa(
                "Maternal antipathy score (8–40) <sup>[3]</sup>",
                self.mother_antipathy(),
            )
            + tr_qa(
                "Maternal neglect score (8–40) <sup>[3]</sup>",
                self.mother_neglect(),
            )
            + tr_qa(
                "Maternal psychological abuse score (0–85) <sup>[4]</sup>",
                self.mother_psychological_abuse(),
            )
            + tr_qa(
                "Paternal antipathy score (8–40) <sup>[3]</sup>",
                self.father_antipathy(),
            )
            + tr_qa(
                "Paternal neglect score (8–40) <sup>[3]</sup>",
                self.father_neglect(),
            )
            + tr_qa(
                "Paternal psychological abuse score (0–85) <sup>[4]</sup>",
                self.father_psychological_abuse(),
            )
            + tr_qa(
                "Role reversal score (17–85) <sup>[5]</sup>",
                self.role_reversal(),
            )
            + tr_qa(
                "Physical abuse screen (0–1) <sup>[6]</sup>",
                self.physical_abuse_screen(),
            )
            + tr_qa(
                "Maternal physical abuse severity score (0–4) "
                "<sup>[6]</sup>",
                self.physical_abuse_severity_mother(),
            )
            + tr_qa(
                "Paternal physical abuse severity score (0–4) "
                "<sup>[6]</sup>",
                self.physical_abuse_severity_father(),
            )
            + tr_qa(
                "Sexual abuse screen (0–3) <sup>[7]</sup>",
                self.sexual_abuse_screen(),
            )
            + tr_qa(
                "First sexual abuse severity score (0–7) <sup>[7]</sup>",
                self.sexual_abuse_score_first(),
            )
            + tr_qa(
                "Other sexual abuse severity score (0–7) <sup>[7]</sup>",
                self.sexual_abuse_score_other(),
            )
            + f"""
                    </table>
                </div>
                <table class="{CssClass.TASKDETAIL}">
            """
            + subheading_spanning_two_columns("1A: " + wxstring("1a_q"))
            + subsubheading_from_wstring("1a_motherfigures")
            + wstring_boolean(
                "1a_mf_birthmother", self.s1a_motherfigure_birthmother
            )
            + wstring_boolean(
                "1a_mf_stepmother", self.s1a_motherfigure_stepmother
            )
            + wstring_boolean(
                "1a_mf_femalerelative", self.s1a_motherfigure_femalerelative
            )
            + string_string(
                "(Female relative details)",
                self.s1a_motherfigure_femalerelative_detail,
            )
            + wstring_boolean(
                "1a_mf_familyfriend", self.s1a_motherfigure_familyfriend
            )
            + wstring_boolean(
                "1a_mf_fostermother", self.s1a_motherfigure_fostermother
            )
            + wstring_boolean(
                "1a_mf_adoptivemother", self.s1a_motherfigure_adoptivemother
            )
            + wstring_boolean("other", self.s1a_motherfigure_other)
            + string_string(
                "(Other, details)", self.s1a_motherfigure_other_detail
            )
            + subsubheading_from_wstring("1a_fatherfigures")
            + wstring_boolean(
                "1a_ff_birthfather", self.s1a_fatherfigure_birthfather
            )
            + wstring_boolean(
                "1a_ff_stepfather", self.s1a_fatherfigure_stepfather
            )
            + wstring_boolean(
                "1a_ff_malerelative", self.s1a_fatherfigure_malerelative
            )
            + string_string(
                "(Male relative details)",
                self.s1a_fatherfigure_malerelative_detail,
            )
            + wstring_boolean(
                "1a_ff_familyfriend", self.s1a_fatherfigure_familyfriend
            )
            + wstring_boolean(
                "1a_ff_fosterfather", self.s1a_fatherfigure_fosterfather
            )
            + wstring_boolean(
                "1a_ff_adoptivefather", self.s1a_fatherfigure_adoptivefather
            )
            + wstring_boolean("other", self.s1a_fatherfigure_other)
            + string_string(
                "(Other, details)", self.s1a_fatherfigure_other_detail
            )
            + subheading_from_string("1B: " + wxstring("1b_q"))
            + wstring_boolean("1b_q", self.s1b_institution)
            + wstring_numeric("1b_q_how_long", self.s1b_institution_time_years)
            + subheading_from_string("1C: " + wxstring("1c_heading"))
            + subsubheading_from_wstring("mother")
            + string_boolean("Mother died before age 17", self.s1c_mother_died)
            + wstring_numeric(
                "1c_parentdiedage", self.s1c_mother_died_subject_aged
            )
            + string_boolean(
                "Separated from mother for >1y", self.s1c_separated_from_mother
            )
            + wstring_numeric(
                "1c_age_first_separated",
                self.s1c_first_separated_from_mother_aged,
            )
            + wstring_numeric(
                "1c_how_long_separation",
                self.s1c_mother_how_long_first_separation_years,
            )
            + wstring_dict(
                "1c_separation_reason",
                self.s1c_mother_separation_reason,
                separation_map,
            )
            + subsubheading_from_wstring("father")
            + string_boolean("Father died before age 17", self.s1c_father_died)
            + wstring_numeric(
                "1c_parentdiedage", self.s1c_father_died_subject_aged
            )
            + string_boolean(
                "Separated from father for >1y", self.s1c_separated_from_father
            )
            + wstring_numeric(
                "1c_age_first_separated",
                self.s1c_first_separated_from_father_aged,
            )
            + wstring_numeric(
                "1c_how_long_separation",
                self.s1c_father_how_long_first_separation_years,
            )
            + wstring_dict(
                "1c_separation_reason",
                self.s1c_father_separation_reason,
                separation_map,
            )
            + wstring_string(
                "please_describe_experience", self.s1c_describe_experience
            )
            + subheading_from_string("2A: " + wxstring("2a_heading"))
            + wstring_dict(
                "2a_which", self.s2a_which_mother_figure, motherfigure_map
            )
            + wstring_string(
                "rnc_if_other_describe",
                self.s2a_which_mother_figure_other_detail,
            )
        )
        for i in range(1, 17):
            html += string_dict(
                str(i) + ". " + wxstring("2a_q" + str(i)),
                getattr(self, "s2a_q" + str(i)),
                no_yes_5way_map,
            )
        html += wstring_string(
            "2a_add_anything", self.s2a_extra
        ) + subheading_from_string("2B: " + wxstring("2b_heading"))
        for i in range(1, 18):
            html += tr(
                str(i) + ". " + wxstring("2b_q" + str(i)),
                answer(
                    get_from_dict(
                        no_yes_3way_map, getattr(self, "s2b_q" + str(i))
                    )
                )
                + " ("
                + answer(
                    get_from_dict(
                        frequency_map,
                        getattr(self, "s2b_q" + str(i) + "_frequency"),
                    )
                )
                + ")",
            )
        html += (
            wstring_boolean("if_any_what_age", self.s2b_age_began)
            + wstring_string("is_there_more_you_want_to_say", self.s2b_extra)
            + subheading_from_string("3A: " + wxstring("3a_heading"))
            + wstring_dict(
                "2a_which", self.s3a_which_father_figure, fatherfigure_map
            )
            + wstring_string(
                "rnc_if_other_describe",
                self.s3a_which_father_figure_other_detail,
            )
        )
        for i in range(1, 17):
            html += string_dict(
                str(i) + ". " + wxstring("3a_q" + str(i)),
                getattr(self, "s3a_q" + str(i)),
                no_yes_5way_map,
            )
        html += wstring_string(
            "3a_add_anything", self.s3a_extra
        ) + subheading_from_string("3B: " + wxstring("3b_heading"))
        for i in range(1, 18):
            html += tr(
                str(i) + ". " + wxstring("3b_q" + str(i)),
                answer(
                    get_from_dict(
                        no_yes_3way_map, getattr(self, "s3b_q" + str(i))
                    )
                )
                + " ("
                + answer(
                    get_from_dict(
                        frequency_map,
                        getattr(self, "s3b_q" + str(i) + "_frequency"),
                    )
                )
                + ")",
            )
        html += (
            wstring_boolean("if_any_what_age", self.s3b_age_began)
            + wstring_string("is_there_more_you_want_to_say", self.s3b_extra)
            + subheading_from_string("3C: " + wxstring("3c_heading"))
        )
        for i in range(1, 18):
            html += string_dict(
                str(i) + ". " + wxstring("3c_q" + str(i)),
                getattr(self, "s3c_q" + str(i)),
                no_yes_5way_map,
            )
        html += (
            wstring_dict(
                "3c_which_parent_cared_for",
                self.s3c_which_parent_cared_for,
                parent_cared_for_map,
            )
            + wstring_boolean(
                "3c_parent_mental_problem", self.s3c_parent_mental_problem
            )
            + wstring_boolean(
                "3c_parent_physical_problem", self.s3c_parent_physical_problem
            )
            + subheading_from_string("4: " + wxstring("4_heading"))
            + subsubheading_from_string("(Adult confidant)")
            + wstring_boolean("4a_q", self.s4a_adultconfidant)
            + subsubheading_from_wstring("4_if_so_who")
            + wstring_boolean(
                "4a_option_mother", self.s4a_adultconfidant_mother
            )
            + wstring_boolean(
                "4a_option_father", self.s4a_adultconfidant_father
            )
            + wstring_boolean(
                "4a_option_relative", self.s4a_adultconfidant_otherrelative
            )
            + wstring_boolean(
                "4a_option_friend", self.s4a_adultconfidant_familyfriend
            )
            + wstring_boolean(
                "4a_option_responsibleadult",
                self.s4a_adultconfidant_responsibleadult,
            )
            + wstring_boolean("4a_option_other", self.s4a_adultconfidant_other)
            + string_string(
                "(Other, details)", self.s4a_adultconfidant_other_detail
            )
            + wstring_string(
                "4_note_anything", self.s4a_adultconfidant_additional
            )
            + subsubheading_from_string("(Child confidant)")
            + wstring_boolean("4b_q", self.s4b_childconfidant)
            + subsubheading_from_wstring("4_if_so_who")
            + wstring_boolean(
                "4b_option_sister", self.s4b_childconfidant_sister
            )
            + wstring_boolean(
                "4b_option_brother", self.s4b_childconfidant_brother
            )
            + wstring_boolean(
                "4b_option_relative", self.s4b_childconfidant_otherrelative
            )
            + wstring_boolean(
                "4b_option_closefriend", self.s4b_childconfidant_closefriend
            )
            + wstring_boolean(
                "4b_option_otherfriend", self.s4b_childconfidant_otherfriend
            )
            + wstring_boolean("4b_option_other", self.s4b_childconfidant_other)
            + string_string(
                "(Other, details)", self.s4b_childconfidant_other_detail
            )
            + wstring_string(
                "4_note_anything", self.s4b_childconfidant_additional
            )
            + subsubheading_from_wstring("4c_q")
            + wstring_boolean("4c_option_mother", self.s4c_closest_mother)
            + wstring_boolean("4c_option_father", self.s4c_closest_father)
            + string_boolean("4c_option_sibling", self.s4c_closest_sibling)
            + wstring_boolean(
                "4c_option_relative", self.s4c_closest_otherrelative
            )
            + wstring_boolean(
                "4c_option_adultfriend", self.s4c_closest_adultfriend
            )
            + wstring_boolean(
                "4c_option_youngfriend", self.s4c_closest_childfriend
            )
            + wstring_boolean("4c_option_other", self.s4c_closest_other)
            + string_string("(Other, details)", self.s4c_closest_other_detail)
            + wstring_string("4_note_anything", self.s4c_closest_additional)
            + subheading_from_string("4: " + wxstring("5_heading"))
            + wstring_boolean("5_mainq", self.s5c_physicalabuse)
            + subsubheading_from_wstring("5_motherfigure")
            + wstring_boolean(
                "5_did_this_person_hurt_you", self.s5c_abused_by_mother
            )
            + wstring_numeric("5_how_old", self.s5c_mother_abuse_age_began)
            + wstring_boolean(
                "5_hit_more_than_once", self.s5c_mother_hit_more_than_once
            )
            + wstring_dict("5_how_hit", self.s5c_mother_hit_how, hitting_map)
            + wstring_boolean("5_injured", self.s5c_mother_injured)
            + wstring_boolean("5_outofcontrol", self.s5c_mother_out_of_control)
            + subsubheading_from_wstring("5_fatherfigure")
            + wstring_boolean(
                "5_did_this_person_hurt_you", self.s5c_abused_by_father
            )
            + wstring_numeric("5_how_old", self.s5c_father_abuse_age_began)
            + wstring_boolean(
                "5_hit_more_than_once", self.s5c_father_hit_more_than_once
            )
            + wstring_dict("5_how_hit", self.s5c_father_hit_how, hitting_map)
            + wstring_boolean("5_injured", self.s5c_father_injured)
            + wstring_boolean("5_outofcontrol", self.s5c_father_out_of_control)
            + wstring_string(
                "5_can_you_describe_1", self.s5c_parental_abuse_description
            )
            + subsubheading_from_string("(Other in household)")
            + wstring_boolean("5_anyone_else", self.s5c_abuse_by_nonparent)
            + wstring_string(
                "5_can_you_describe_2", self.s5c_nonparent_abuse_description
            )
            + subheading_from_string("6: " + wxstring("6_heading"))
            + wstring_dict(
                "6_any_unwanted",
                self.s6_any_unwanted_sexual_experience,
                no_yes_3way_map,
            )
            + wstring_dict(
                "6_intercourse", self.s6_unwanted_intercourse, no_yes_3way_map
            )
            + wstring_dict(
                "6_upset_adult_authority",
                self.s6_upsetting_sexual_adult_authority,
                no_yes_3way_map,
            )
            + subsubheading_from_wstring("6_first_experience")
            + wstring_numeric("6_q1", self.s6_first_age)
            + wstring_boolean("6_q2", self.s6_first_person_known)
            + wstring_boolean("6_q3", self.s6_first_relative)
            + wstring_boolean("6_q4", self.s6_first_in_household)
            + wstring_boolean("6_q5", self.s6_first_more_than_once)
            + wstring_boolean("6_q6", self.s6_first_touch_privates_subject)
            + wstring_boolean("6_q7", self.s6_first_touch_privates_other)
            + wstring_boolean("6_q8", self.s6_first_intercourse)
            + subsubheading_from_wstring("6_other_experience")
            + wstring_numeric("6_q1", self.s6_other_age)
            + wstring_boolean("6_q2", self.s6_other_person_known)
            + wstring_boolean("6_q3", self.s6_other_relative)
            + wstring_boolean("6_q4", self.s6_other_in_household)
            + wstring_boolean("6_q5", self.s6_other_more_than_once)
            + wstring_boolean("6_q6", self.s6_other_touch_privates_subject)
            + wstring_boolean("6_q7", self.s6_other_touch_privates_other)
            + wstring_boolean("6_q8", self.s6_other_intercourse)
            + wstring_string(
                "5_can_you_describe_1", self.s6_unwanted_sexual_description
            )
            + subheading_from_wstring("any_other_comments")
            + wstring_string("any_other_comments", self.any_other_comments)
            + f"""
                </table>
                <div class="{CssClass.FOOTNOTES}">
                    [1] Death of mother/father before age 17 or continuous
                        separation of ≥1 year.
                    [2] Reason for loss ‘abandonment’ or ‘other’.
                    [3] Section 2A and 3A contain antipathy and neglect scales.
                        Antipathy scales: reverse items 8, 11;
                        then sum 1, 4, 6, 8–11, 16.
                        Neglect scales: reverse items 2, 3, 5, 12–14;
                        then sum 2, 3, 5, 7, 12–15.
                    [4] Psychological abuse scores (sections 2B, 3B):
                        sum of items 1–17 (yes=2, unsure=1, no=0) with
                        frequencies (never=0, once=1, rarely=2, often=3).
                        Any score &gt;1 is a risk indicator.
                    [5] Role reversal: sum of scores from section 3C.
                    [6] Physical abuse (section 4): first question (screen
                        item) is scored 0/1. For each parent, score 1 for
                        {{&gt;1 occasion, {{belt/stick/punch/kick}}, injured,
                        out-of-control}}.
                    [7] Sexual abuse (section 6): no=0, unsure=1, yes=1.
                    First three questions are the screen.
                </div>
            """
        )
        return html


# =============================================================================
# More helper functions
# =============================================================================


def subheading_from_string(s: str) -> str:
    return subheading_spanning_two_columns(s)


def subsubheading_from_string(s: str) -> str:
    return f'<tr><td></td><td class="{CssClass.SUBHEADING}">{s}</td></tr>'


def row(label: str, value: Any, default: str = "") -> str:
    return tr_qa(label, value, default=default)


def string_boolean_(req: CamcopsRequest, string: str, value: Any) -> str:
    return row(string, get_yes_no_none(req, value))


def string_numeric(string: str, value: Any) -> str:
    return row(string, value)


def string_string(string: str, value: str) -> str:
    return row(string, ws.webify(value))


def string_dict(string: str, value: Any, d: Dict) -> str:
    return row(string, get_from_dict(d, value))
