"""
generator_redacted.py - redacted CA Case Document Generator.

This file intentionally keeps the deterministic template-hydration code,
but removes parent-directory path manipulation and any dependency on the
publicsite package tree. It is provided for demonstration only.
"""
import os
from datetime import datetime
from dateutil import parser as dateparser

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))

import openpyxl
from openpyxl.styles import Font, Alignment, PatternFill
from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH

CA_FOLDER = SCRIPT_DIR
TEMPLATE_PATH = os.path.join(CA_FOLDER, "template.docx")

VARIABLE_DEFINITIONS = {
    "case_num": {"description": "Case number (integer, e.g., 22 for HCAL22)", "required": True},
    "case_year": {"description": "Year of the case (4-digit integer)", "required": True},
    "hcal_case_num": {"description": "HCAL case number from Court of First Instance (integer)", "required": False},
    "hcal_case_year": {"description": "HCAL year from Court of First Instance (4-digit integer)", "required": False},
    "applicant_name": {"description": "Full name(s) of the applicant(s). For multiple applicants, use comma-separated names in order as they appear in the judgment", "required": True},
    "applicant_gender": {"description": "Gender: 'male' or 'female'", "required": True},
    "num_applicants": {"description": "Number of applicants (integer, default 1)", "required": False},
    "judge_name": {"description": "Full name of the judge", "required": True},
    "judge_title_has_nrc": {"description": "Judge title includes '(Non-refoulement Claims)': TRUE or FALSE", "required": True},
    "country": {"description": "Country of origin of the Applicant", "required": True},
    "one_line_description": {"description": "Brief description of why applicant fears harm", "required": True},
    "dismissal_order_date": {"description": "Date of the Dismissal Order", "required": True},
    "board_decision_date_1": {"description": "Date of the first Board decision", "required": True},
    "board_decision_date_2": {"description": "Date of the second Board decision (if any)", "required": False},
    "director_decision_date_1": {"description": "Date of the first Director decision", "required": False},
    "director_decision_date_2": {"description": "Date of the second Director decision (if any)", "required": False},
    "notice_of_appeal_date": {"description": "Date the notice of appeal was filed", "required": True},
    "affirmation_date": {"description": "Date of the affirmation/affidavit (if any)", "required": False},
    "oath_type": {"description": "Type of oath: 'affirmation' or 'affidavit'", "required": False},
    "grounds_in_affirmation": {"description": "Grounds in affirmation/affidavit (format: one-liner \"verbatim text\", one per line)", "required": False},
    "grounds_in_affirmation_summary": {"description": "Succinct summary of grounds in affirmation/affidavit", "required": False},
    "form1_has_hyperlink": {"description": "CALL-1 Form has hyperlink to Board decision: TRUE or FALSE", "required": True},
    "form1_reference_type": {"description": "'CALL-1 Form' or 'Judgment'", "required": True},
    "hyperlink_para_num": {"description": "Paragraph number containing hyperlink", "required": False},
    "hyperlink_footnote_num": {"description": "Footnote number containing hyperlink (preferred over para_num if both present)", "required": False},
    "case_summary_para_begin": {"description": "Beginning paragraph of case summary", "required": False},
    "case_summary_para_end": {"description": "Ending paragraph of case summary", "required": False},
    "judge_reasons_para_begin": {"description": "Beginning paragraph of judge's reasons", "required": True},
    "judge_reasons_para_end": {"description": "Ending paragraph of judge's reasons", "required": True},
    "grounds_in_noa": {"description": "Grounds in notice of appeal (format: one-liner \"verbatim text\", one per line)", "required": True},
    "grounds_in_noa_summary": {"description": "Succinct summary of grounds in notice of appeal", "required": True},
    "written_submission_date": {"description": "Date of written submission (if any)", "required": False},
    "written_submission_type": {"description": "Type of written submission document (e.g., 'skeleton argument', 'written submission', 'written submissions')", "required": False},
    "grounds_in_submission": {"description": "Grounds in written submission (format: one-liner \"verbatim text\", one per line)", "required": False},
    "grounds_in_submission_summary": {"description": "Succinct summary of grounds in written submission", "required": False},
    "registrar_direction_date": {"description": "Date of Registrar's direction (if no written submission)", "required": False},
    "hearing_date_scheduled": {"description": "Scheduled hearing date (if no written submission)", "required": False},
    "applicant_failed_to_raise_ground": {"description": "TRUE if the Judge mentioned that the Applicant completely failed to raise any grounds in the Form 86 or affirmation, otherwise FALSE", "required": False},
}


def parse_date(date_str) -> datetime:
    if not date_str or str(date_str).strip().upper() in ["", "NULL", "NONE"]:
        return None
    if isinstance(date_str, datetime):
        return date_str
    return dateparser.parse(str(date_str), dayfirst=True)


def format_date(dt: datetime) -> str:
    if not dt:
        return ""
    return dt.strftime("%#d %B %Y") if os.name == 'nt' else dt.strftime("%-d %B %Y")


def get_output_path(base_path: str) -> str:
    if not os.path.exists(base_path):
        return base_path
    base, ext = os.path.splitext(base_path)
    counter = 2
    while os.path.exists(f"{base}-{counter}{ext}"):
        counter += 1
    return f"{base}-{counter}{ext}"


def read_xlsx(xlsx_path: str) -> dict:
    if not os.path.exists(xlsx_path):
        return {}
    wb = openpyxl.load_workbook(xlsx_path)
    ws = wb.active
    data = {}
    for row in ws.iter_rows(min_row=2):
        var_name = row[0].value
        value = row[2].value
        if var_name:
            data[var_name] = value
    wb.close()
    return data


def write_xlsx(xlsx_path: str, data: dict):
    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = "Case Data"
    
    headers = ["Variable Name", "Description", "Value", "Required"]
    header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
    header_font = Font(bold=True, color="FFFFFF")
    
    for col, header in enumerate(headers, 1):
        cell = ws.cell(row=1, column=col, value=header)
        cell.fill = header_fill
        cell.font = header_font
        cell.alignment = Alignment(horizontal="center")
    
    row_num = 2
    for var_name, var_def in VARIABLE_DEFINITIONS.items():
        ws.cell(row=row_num, column=1, value=var_name)
        ws.cell(row=row_num, column=2, value=var_def["description"])
        value = data.get(var_name, "")
        if isinstance(value, datetime):
            value = format_date(value)
        ws.cell(row=row_num, column=3, value=str(value) if value else "")
        ws.cell(row=row_num, column=4, value="TRUE" if var_def["required"] else "FALSE")
        row_num += 1
    
    ws.column_dimensions['A'].width = 25
    ws.column_dimensions['B'].width = 60
    ws.column_dimensions['C'].width = 40
    ws.column_dimensions['D'].width = 12
    wb.save(xlsx_path)
    wb.close()


def prompt_for_value(var_name: str, var_def: dict) -> str:
    print(f"\n{var_name}")
    print(f"  {var_def['description']}")
    return input("  Enter value: ").strip()


def prompt_for_missing(data: dict) -> dict:
    for var_name, var_def in VARIABLE_DEFINITIONS.items():
        current = data.get(var_name)
        is_empty = current is None or str(current).strip().upper() in ["", "NULL", "NONE"]
        
        if var_def["required"] and is_empty:
            value = prompt_for_value(var_name, var_def)
            if not value:
                raise ValueError(f"Required field '{var_name}' cannot be empty")
            data[var_name] = value
    return data


def validate_and_prompt_conditional(data: dict) -> dict:
    has_hyperlink = str(data.get("form1_has_hyperlink", "")).upper() == "TRUE"
    has_written_sub = bool(parse_date(data.get("written_submission_date")))
    
    if has_hyperlink and not data.get("hyperlink_footnote_num") and not data.get("hyperlink_para_num"):
        data["hyperlink_para_num"] = prompt_for_value("hyperlink_para_num", VARIABLE_DEFINITIONS["hyperlink_para_num"])
    
    if not has_hyperlink:
        if not data.get("case_summary_para_begin"):
            data["case_summary_para_begin"] = prompt_for_value("case_summary_para_begin", VARIABLE_DEFINITIONS["case_summary_para_begin"])
        if not data.get("case_summary_para_end"):
            data["case_summary_para_end"] = prompt_for_value("case_summary_para_end", VARIABLE_DEFINITIONS["case_summary_para_end"])
    
    if not has_written_sub:
        if not data.get("registrar_direction_date"):
            data["registrar_direction_date"] = prompt_for_value("registrar_direction_date", VARIABLE_DEFINITIONS["registrar_direction_date"])
        if not data.get("hearing_date_scheduled"):
            data["hearing_date_scheduled"] = prompt_for_value("hearing_date_scheduled", VARIABLE_DEFINITIONS["hearing_date_scheduled"])
    else:
        if not data.get("grounds_in_submission_summary"):
            data["grounds_in_submission_summary"] = prompt_for_value("grounds_in_submission_summary", VARIABLE_DEFINITIONS["grounds_in_submission_summary"])
        if not data.get("grounds_in_submission"):
            data["grounds_in_submission"] = prompt_for_value("grounds_in_submission", VARIABLE_DEFINITIONS["grounds_in_submission"])
    
    return data


def strip_list_labels(text: str) -> str:
    """Remove (a), (b), (c) etc. list labels from summary text, leaving semicolon-separated grounds."""
    import re
    if not text:
        return text
    # Remove optional "and " prefix then the "(x) " label pattern
    text = re.sub(r'(?:and\s+)?\([a-z]\)\s*', '', text)
    # Clean up any resulting multiple spaces
    text = re.sub(r' {2,}', ' ', text).strip()
    return text


# --------------------------------------------------------------------------- #
# Writer registry
# --------------------------------------------------------------------------- #
# A "writer_judge" is a house style for the note/judgment prose.  All writers
# consume the SAME VARIABLE_DEFINITIONS (extracted once, upstream) and the SAME
# derived context (`_build_context`); they differ only in wording.  To add a
# new judge's style, write a `_writer_<name>(data, c)` function returning a list
# of paragraph items and register it in WRITERS below.
#
# A paragraph item is either:
#   * a str                       -> ordinary numbered paragraph
#   * ("CITED", text)             -> block-quote (verbatim applicant grounds)
DEFAULT_WRITER_JUDGE = "chu"


class _Ctx:
    """Lightweight attribute bag for the derived context."""
    def __init__(self, **kw):
        self.__dict__.update(kw)


def _build_context(data: dict) -> _Ctx:
    """Derive every wording-agnostic helper shared by all writer styles.

    Anything here is common across writer_judge values; only the final prose
    (which capitalisation, which defined terms, which citations) lives in the
    individual `_writer_*` functions.
    """
    dismissal_date = parse_date(data.get("dismissal_order_date"))
    board_date_1 = parse_date(data.get("board_decision_date_1"))
    board_date_2 = parse_date(data.get("board_decision_date_2"))
    director_date_1 = parse_date(data.get("director_decision_date_1"))
    director_date_2 = parse_date(data.get("director_decision_date_2"))
    noa_date = parse_date(data.get("notice_of_appeal_date"))
    affirmation_date = parse_date(data.get("affirmation_date"))
    written_sub_date = parse_date(data.get("written_submission_date"))
    written_sub_type = data.get("written_submission_type") or "written submission"
    registrar_date = parse_date(data.get("registrar_direction_date"))
    hearing_date = parse_date(data.get("hearing_date_scheduled"))

    num_applicants = int(data.get("num_applicants") or 1)
    is_plural = num_applicants > 1
    gender = str(data.get("applicant_gender", "male")).lower()

    if is_plural:
        he, He = "they", "They"
        his, His = "their", "Their"
        him, Him = "them", "Them"
    elif gender == "female":
        he, He = "she", "She"
        his, His = "her", "Her"
        him, Him = "her", "Her"
    else:
        he, He = "he", "He"
        his, His = "his", "His"
        him, Him = "him", "Him"

    # Capitalised ("Applicant") vs lower ("applicant") noun forms.  Different
    # judges prefer different capitalisation, so both are exposed.
    Applicant = "Applicants" if is_plural else "Applicant"
    applicant = "applicants" if is_plural else "applicant"
    Applicant_poss = "Applicants’" if is_plural else "Applicant’s"
    applicant_poss = "applicants’" if is_plural else "applicant’s"

    # Verb forms for singular/plural agreement.
    has_have = "have" if is_plural else "has"
    is_are = "are" if is_plural else "is"
    appeals_verb = "appeal" if is_plural else "appeals"
    contends_verb = "contend" if is_plural else "contends"
    claims_verb = "claim" if is_plural else "claims"
    submits_verb = "submit" if is_plural else "submits"
    complains_verb = "complain" if is_plural else "complains"
    returns_verb = "return" if is_plural else "returns"

    num_board = 2 if board_date_2 else 1
    num_director = 2 if director_date_2 else 1

    has_nrc = str(data.get("judge_title_has_nrc", "")).upper() == "TRUE"
    judge_title = "Deputy High Court Judge (Non-refoulement Claims)" if has_nrc else "Deputy High Court Judge"

    has_hyperlink = str(data.get("form1_has_hyperlink", "")).upper() == "TRUE"
    has_written_sub = bool(written_sub_date)
    form1_ref = data.get("form1_reference_type") or "CALL-1 Form"

    if num_board == 2:
        board_dates_str = f"{format_date(board_date_1)} and {format_date(board_date_2)}"
        board_decision_word = "decisions"
        that_those = "those"
    else:
        board_dates_str = format_date(board_date_1)
        board_decision_word = "decision"
        that_those = "that"

    director_decision_word = "decisions" if num_director == 2 else "decision"
    if num_director == 2:
        director_dates_str = f"{format_date(director_date_1)} and {format_date(director_date_2)}"
    else:
        director_dates_str = format_date(director_date_1)

    nrc_claim_word = "claims" if is_plural else "claim"
    national_word = "nationals" if is_plural else "a national"

    # Normalise the TRUE/FALSE flag (a literal "FALSE" string is truthy, so
    # never test the raw value for truthiness).
    failed_to_raise_ground = str(data.get("applicant_failed_to_raise_ground", "")).upper() == "TRUE"

    return _Ctx(
        # dates (raw datetimes + formatted)
        dismissal_date=dismissal_date, board_date_1=board_date_1,
        director_date_1=director_date_1, noa_date=noa_date,
        affirmation_date=affirmation_date, written_sub_date=written_sub_date,
        registrar_date=registrar_date, hearing_date=hearing_date,
        board_dates_str=board_dates_str, director_dates_str=director_dates_str,
        # people / verbs
        he=he, He=He, his=his, His=His, him=him, Him=Him,
        Applicant=Applicant, applicant=applicant,
        Applicant_poss=Applicant_poss, applicant_poss=applicant_poss,
        has_have=has_have, is_are=is_are, appeals_verb=appeals_verb,
        contends_verb=contends_verb, claims_verb=claims_verb,
        submits_verb=submits_verb, complains_verb=complains_verb,
        returns_verb=returns_verb,
        # counts / words
        num_board=num_board, num_director=num_director,
        board_decision_word=board_decision_word,
        director_decision_word=director_decision_word,
        that_those=that_those, nrc_claim_word=nrc_claim_word,
        national_word=national_word, failed_to_raise_ground=failed_to_raise_ground,
        # judge / form
        judge_title=judge_title, form1_ref=form1_ref,
        has_hyperlink=has_hyperlink, has_written_sub=has_written_sub,
        written_sub_type=written_sub_type,
        oath_type=data.get("oath_type") or "affirmation",
        # summaries (letter-labels stripped)
        noa_summary=strip_list_labels(str(data.get('grounds_in_noa_summary') or '')),
        affirmation_summary=strip_list_labels(str(data.get('grounds_in_affirmation_summary') or '')),
        submission_summary=strip_list_labels(str(data.get('grounds_in_submission_summary') or '')),
    )


def _hyperlink_or_summary_clause(data: dict, c: _Ctx) -> str:
    """Where the basis of the claim can be found - shared by both styles."""
    if c.has_hyperlink:
        footnote_num = data.get('hyperlink_footnote_num')
        para_num = data.get('hyperlink_para_num')
        if footnote_num:
            return f"which may be viewed online via the hyperlink contained in footnote {footnote_num} of the {c.form1_ref}."
        return f"which may be viewed online via the hyperlink contained in [{para_num}] of the {c.form1_ref}."
    return f"which has been summarised by the Judge at [{data.get('case_summary_para_begin')}] - [{data.get('case_summary_para_end')}] of the {c.form1_ref}."


# --------------------------------------------------------------------------- #
# Writer: achan  (original house style - "the Applicant", defined terms bolded)
# --------------------------------------------------------------------------- #

def _writer_achan(data: dict, c: _Ctx) -> list:
    A, A_poss = c.Applicant, c.Applicant_poss
    board_decision_defined = "Board’s Decisions" if c.num_board == 2 else "Board’s Decision"
    lines = []

    lines.append(
        f"This is the {A_poss} appeal against the order (“Dismissal Order”) of {c.judge_title} {data.get('judge_name')} (“Judge”) dated {format_date(c.dismissal_date)} by which {c.his} application for leave to apply for judicial review (“Leave Application”) against the {c.board_decision_word} of the Torture Claims Appeal Board (“Board”) dated {c.board_dates_str} (“{board_decision_defined}”) was dismissed.  By {c.that_those} {c.board_decision_word}, the Board upheld the {c.director_decision_word} of the Director of Immigration (“Director”) to reject the {A_poss} non-refoulement {c.nrc_claim_word}."
    )

    p2_start = f"The {A} {c.is_are} {c.national_word} of {data.get('country')}.  The basis of {c.his} non-refoulement {c.nrc_claim_word} had been set out in detail in the {board_decision_defined}, "
    p2_max = f"  In gist, the {A} {c.claims_verb} that if {c.he} returns home, {c.he} will be {data.get('one_line_description')}"
    lines.append(p2_start + _hyperlink_or_summary_clause(data, c) + p2_max)

    lines.append(f"In [{data.get('judge_reasons_para_begin')}] to [{data.get('judge_reasons_para_end')}] of the {c.form1_ref}, the Judge gave detailed reasons for refusing the Leave Application.")

    p4_meat = f" and the {c.oath_type} filed on {format_date(c.affirmation_date)}" if c.affirmation_date else ""
    lines.append(f"By a Notice of Appeal filed on {format_date(c.noa_date)}{p4_meat}, the {A} {c.appeals_verb} against the Judge’s decision.  In summary, the {A} {c.contends_verb} that: {c.noa_summary}.")
    if data.get('grounds_in_noa'):
        lines.append(f"The {A} contends that:")
        lines.append(("CITED", '"' + data.get('grounds_in_noa') + '"'))

    if c.affirmation_date and data.get('grounds_in_affirmation_summary'):
        lines.append(f"In the {c.oath_type} filed in support of the Notice of Appeal, the {A} deposed that {c.affirmation_summary}.")
        if data.get('grounds_in_affirmation'):
            lines.append(f"The {A} deposed that:")
            lines.append(("CITED", '“' + data.get('grounds_in_affirmation') + '”'))

    if not c.has_written_sub:
        lines.append(f"This appeal was scheduled to be heard on {format_date(c.hearing_date)}.  The {A} {c.has_have} failed to lodge any skeleton argument in support of {c.his} appeal in accordance with the directions given by the Registrar of Civil Appeals on {format_date(c.registrar_date)} (“Directions”).  Accordingly, the {A} {c.is_are} deemed to have waived {c.his} right to have an oral hearing of the appeal and elected to have the appeal disposed of on paper.  Having considered the documents before us, we consider that it is appropriate to deal with the {A_poss} appeal on paper without an oral hearing.")
    else:
        lines.append(f"In {c.his} {c.written_sub_type} lodged on {format_date(c.written_sub_date)}, the {A} {c.submits_verb} that {c.submission_summary}.")
    if data.get('grounds_in_submission'):
        lines.append(f"The {A} submits that:")
        lines.append(("CITED", '“' + data.get('grounds_in_submission') + '”'))

    lines.append("The general approach of this Court in dealing with appeals in non-refoulement cases has been set out in *Nupur Mst v Director of Immigration* [2018] HKCA 524 at [14].  In particular, in an appeal against refusal of leave to apply for judicial review in non-refoulement cases, this Court would only examine the decision of the judge in light of the grounds advanced by the applicant.  If no viable ground is put forward to reverse the judge’s decision, the appeal should be dismissed.  This Court’s role is not to examine the Board’s decision afresh as if it is a fresh application for judicial review (see *Nupur Mst* at [14(6)]).")

    lines.append("Further, the assessment of evidence, Country of Origin information, risk of harm, state protection and viability of internal relocation are primarily within the province of the Board and the Director. The Court will not intervene by way of judicial review unless there is an error of law or procedural unfairness or irrationality in the decision of the Board: *Re Kartini* [2019] HKCA 1022 at [13].")

    lines.append(f"In the Notice of Appeal, the {A} simply stated that {c.he} did not agree with the Dismissal Order but {c.has_have} failed to identify any error committed by the Judge.  {c.His} contention that the decision makers failed to scrutinize {c.his} problem is not supported by any particulars about the alleged failure or problem.  {c.He} {c.has_have} therefore failed to advance any viable ground of appeal.")

    p7addendum = ""
    if c.failed_to_raise_ground:
        p7addendum = f"we note that the matters in those grounds had never been raised in the Form 86 or the affirmation filed in support of the Form 86.  The {A} {c.has_have} not given any reason why {c.he} should be allowed to raise these grounds for the first time at the appeal stage, nor can we find any good reason to allow {c.him} to do so.  We therefore do not allow the {A} to rely on those grounds and we place no weight on them."
    lines.append("In respect of the grounds of appeal, " + p7addendum)

    lines.append(f"??we note that the Board dismissed the {A_poss} non-refoulement {c.nrc_claim_word} primarily on the ground that {c.his} evidence was not credible ({board_decision_defined}, [??]) and that {c.he} did not face any risk of harm from _.  In the circumstances, there was no need for the Board to consider _ in detail and the issue of ? did not arise.  There is thus no merit in any of the grounds of appeal advanced by the {A}")
    lines.append(f"??We note that most of the grounds of appeal are concerned with alleged errors made by the Board and the Director, and not with any error of the Judge.  As for the ground based on the Judge’s failure to consider _, this issue was never raised in the Form 86 or the affirmation filed in support of the Form 86.  It is impermissible for the {A} to raise fact-sensitive issue for the first time on appeal.  In the premises, the {A} {c.has_have} failed to advance any viable ground of appeal.")
    lines.append(f"??In respect of the grounds of appeal, we note that the issues pertaining to _  had been addressed by the Judge in [?] and [?] of the CALL-1 Form and the {A} {c.has_have} not identified any error in the Judge’s reasoning.  Regarding the lack of language assistance, it has been repeatedly emphasised by this Court that as a matter of law, a non-refoulement claimant is not entitled to free legal representation at all stages of the process and that the high standard of fairness required by law does not entail interpretation service being made available to an applicant all the time as he desires: *Re Pante Luisa Tuppil* [2025] HKCA 1123 at [11].   We therefore do not see any merit in the grounds of appeal.")

    lines.append("In the premises, this appeal is dismissed with no order as to costs.")
    lines.append("Prepared by [redacted]\nTemplate version 2026-03-16 Adapted from CACV914/2025 (*per* Anthony Chan JA) and CACV912/2025 (*per* H. Au-Yeung J)")
    return lines


# --------------------------------------------------------------------------- #
# Writer: chu  (Hon Chu VP house style - lower-case "applicant", "the Judge" /
# "the Board" / "the Director" defined terms, Chu's citation set.  Modelled on
# CACV 378/2026.  NB: per instruction, the legal-aid / 42-day stay passage is
# deliberately NOT reproduced here.)
# --------------------------------------------------------------------------- #

def _writer_chu(data: dict, c: _Ctx) -> list:
    a, a_poss = c.applicant, c.applicant_poss
    board_dec = "the Board’s decisions" if c.num_board == 2 else "the Board’s decision"
    lines = []

    director_clause = f" dated {c.director_dates_str}" if c.director_dates_str else ""
    lines.append(
        f"This is the {a_poss} appeal against the decision of {c.judge_title} {data.get('judge_name')} (“the Judge”) given on {format_date(c.dismissal_date)} refusing {c.his} application for leave to apply for judicial review (“Leave Application”) against the {c.board_decision_word} of the Torture Claims Appeal Board (“the Board”) dated {c.board_dates_str}.  The Board had dismissed the {a_poss} appeal against the {c.director_decision_word} of the Director of Immigration (“the Director”){director_clause}, which in turn had rejected the {a_poss} non-refoulement {c.nrc_claim_word}."
    )

    p2_start = f"The {a} {c.is_are} {c.national_word} of {data.get('country')}.  The basis of {c.his} non-refoulement {c.nrc_claim_word} had been set out in detail in {board_dec}, "
    p2_max = f"  In gist, the {a} {c.claims_verb} that if {c.he} {c.returns_verb} home, {c.he} will be {data.get('one_line_description')}"
    lines.append(p2_start + _hyperlink_or_summary_clause(data, c) + p2_max)

    lines.append(f"In [{data.get('judge_reasons_para_begin')}] to [{data.get('judge_reasons_para_end')}] of the {c.form1_ref}, the Judge gave detailed reasons for refusing the application for leave.")

    p4_meat = f" and the {c.oath_type} filed on {format_date(c.affirmation_date)}" if c.affirmation_date else ""
    lines.append(f"By a Notice of Appeal filed on {format_date(c.noa_date)}{p4_meat}, the {a} {c.appeals_verb} against the Judge’s decision.  In summary, the {a} {c.contends_verb} that {c.noa_summary}.")
    if data.get('grounds_in_noa'):
        lines.append(f"The {a} {c.contends_verb} that:")
        lines.append(("CITED", '“' + data.get('grounds_in_noa') + '”'))

    if c.affirmation_date and data.get('grounds_in_affirmation_summary'):
        lines.append(f"In the {c.oath_type} filed in support of the Notice of Appeal, the {a} deposed that {c.affirmation_summary}.")
        if data.get('grounds_in_affirmation'):
            lines.append(f"The {a} deposed that:")
            lines.append(("CITED", '“' + data.get('grounds_in_affirmation') + '”'))

    if not c.has_written_sub:
        lines.append(f"This appeal was scheduled to be heard on {format_date(c.hearing_date)}.  The {a} {c.has_have} failed to lodge any skeleton argument in support of {c.his} appeal in accordance with the directions given by the Registrar of Civil Appeals on {format_date(c.registrar_date)} (“Directions”).  Accordingly, the {a} {c.is_are} deemed to have waived {c.his} right to an oral hearing of the appeal and elected to have the appeal determined on paper.  Having considered the documents before us, we consider it appropriate to deal with the {a_poss} appeal on paper without an oral hearing.")
    else:
        lines.append(f"The {a} {c.has_have} lodged {c.his} {c.written_sub_type} on {format_date(c.written_sub_date)}, in which {c.he} {c.submits_verb} that {c.submission_summary}.")
    if data.get('grounds_in_submission'):
        lines.append(f"The {a} {c.submits_verb} that:")
        lines.append(("CITED", '“' + data.get('grounds_in_submission') + '”'))

    lines.append("In assessing the merits of the appeal, we shall have regard to the legal principles which this Court has adopted in dealing with appeals in non-refoulement cases: see *Nupur Mst v Director of Immigration* [2018] HKCA 524 at [14]; *Re Md Shohel Sheak* [2018] HKCA 714 at [13]; and *Re Limbu Birkhaman* [2019] HKCA 50 at [11].  In particular, the role of the Court in a judicial review is not to provide a further avenue of appeal.  The Court will not intervene by way of judicial review unless there are errors of law or procedural unfairness or irrationality in the decision of the Board.  In the determination of an appeal against the refusal of leave by the Court of First Instance, the Court of Appeal will only examine the decision of the judge in light of the grounds advanced by the applicant.  If no viable ground is put forward to reverse the judge’s decision, the appeal should be dismissed.  It is not the role of this Court to examine the decision of the Board afresh as if the appeal were a fresh application for judicial review.")

    lines.append("Further, it is well-established that the assessment of evidence, country of origin information (COI), risk of harm, state protection and viability of internal relocation are primarily within the province of the Board and the Director as they are the primary decision makers.  The Court, in its supervisory role, will not intervene by way of judicial review unless there are errors of law, procedural unfairness or irrationality in the decision of the Board: *Re Kartini* [2019] HKCA 1022.")

    lines.append(f"In the present case, although the {a} {c.complains_verb} about the Judge’s decision, {c.he} {c.has_have} not identified any error on the part of the Judge.  {c.His} bare assertion that the decision makers failed to scrutinise {c.his} {c.nrc_claim_word} is not supported by any particulars.  The {a} {c.has_have} therefore failed to advance any viable ground of appeal.")

    if c.failed_to_raise_ground:
        lines.append(f"In respect of the grounds of appeal, we note that the matters raised had never been raised in the Form 86 or the affirmation filed in support of the Form 86.  The {a} {c.has_have} given no reason why {c.he} should be allowed to raise these grounds for the first time on appeal, nor can we find any good reason to allow {c.him} to do so.  We therefore do not allow the {a} to rely on those grounds and place no weight on them.")

    lines.append(f"For the above reasons, the {a_poss} appeal has no merits.  Accordingly, we dismiss the appeal with no order as to costs.")
    return lines


WRITERS = {
    "achan": _writer_achan,
    "chu": _writer_chu,
}


def generate_document(data: dict, writer_judge: str = DEFAULT_WRITER_JUDGE) -> dict:
    """Build the document data structure in the requested house style.

    ``writer_judge`` selects the wording; the underlying facts come from the
    shared ``VARIABLE_DEFINITIONS`` / ``_build_context``.
    """
    writer_judge = (writer_judge or DEFAULT_WRITER_JUDGE).lower()
    if writer_judge not in WRITERS:
        raise ValueError(
            f"Unknown writer_judge {writer_judge!r}; supported: {sorted(WRITERS)}"
        )
    c = _build_context(data)
    lines = WRITERS[writer_judge](data, c)

    return {
        "case_num": data.get("case_num"),
        "case_year": data.get("case_year"),
        "applicant_name": data.get("applicant_name"),
        "writer_judge": writer_judge,
        "paragraphs": lines,
    }


def generate_docx(doc_data: dict, docx_path: str):
    """Generate DOCX using template.docx and adding paragraphs."""
    import re
    
    # Load template
    doc = Document(TEMPLATE_PATH)
    
    # Add centered title: "JDA Note" with Heading 3 style
    title1 = doc.add_paragraph("JDA Note", style="Heading 3")
    title1.alignment = WD_ALIGN_PARAGRAPH.CENTER
    
    # Add centered applicant name with Heading 3 style
    title2 = doc.add_paragraph(doc_data['applicant_name'], style="Heading 3")
    title2.alignment = WD_ALIGN_PARAGRAPH.CENTER
    
    # Add centered title: "CACV {case_num}/{case_year}" with Heading 3 style
    title3 = doc.add_paragraph(f"CACV {doc_data['case_num']}/{doc_data['case_year']}", style="Heading 3")
    title3.alignment = WD_ALIGN_PARAGRAPH.CENTER
    
    # Add numbered paragraphs
    for item in doc_data['paragraphs']:
        # Check if this is a cited paragraph (tuple format)
        if isinstance(item, tuple) and item[0] == "CITED":
            para_text = item[1]
            para = doc.add_paragraph(style="Cited")
        else:
            para_text = item
            para = doc.add_paragraph(style="Sublevel 1 (1)")
        
        # Handle formatting: italics (*...*) and bold defined terms (“...”)
        # Collapse an accidental doubled full stop (e.g. a summary that ends in
        # "." followed by the template's own period -> "..").
        para_text = re.sub(r'\.\s*\.(?!\.)', '.', para_text)
        # Ensure double spaces after periods (but not if already present)
        para_text = re.sub(r'\. (?! )', '.  ', para_text)  # Add double space after period if only single space follows
        
        # Pattern matches *italic* or (“bold”) with smart quotes
        LQUOTE = '\u201c'
        RQUOTE = '\u201d'
        pattern = f'(\\*[^*]+\\*|\\({LQUOTE}[^{RQUOTE}]+{RQUOTE}\\))'
        parts = re.split(pattern, para_text)
        for part in parts:
            if part.startswith('*') and part.endswith('*'):
                # Italic text
                run = para.add_run(part[1:-1])
                run.italic = True
            elif part.startswith('(' + LQUOTE) and part.endswith(RQUOTE + ')'):
                # Bold defined term - parens and quotes normal, inner text bold
                inner = part[2:-2]
                para.add_run('(' + LQUOTE)
                run = para.add_run(inner)
                run.bold = True
                para.add_run(RQUOTE + ')')
            else:
                # Normal text
                para.add_run(part)
    
    # Add empty line before signature
    doc.add_paragraph()
    doc.save(docx_path)


def generate_markdown(doc_data: dict) -> str:
    """Generate markdown string from document data."""
    lines = []
    lines.append("<center>")
    lines.append("**JDA Note**")
    lines.append(f"**{doc_data['applicant_name']}**")
    lines.append(f"**CACV {doc_data['case_num']}/{doc_data['case_year']}**")
    lines.append("</center>")
    lines.append("")
    
    numbered_lines = [f"{i+1}. {para}" for i, para in enumerate(doc_data['paragraphs'])]
    lines.extend(numbered_lines)
    
    lines.append("")
    lines.append("Prepared by [redacted]")
    
    return "\n\n".join(lines)


def run_generator(case_num: int, case_year: int, writer_judge: str = DEFAULT_WRITER_JUDGE):
    """Run the generator for a specific case. Can be called from extractor.py.

    ``writer_judge`` selects the house style (see WRITERS).
    """
    base_name = f"CACV{case_num}-{case_year}"
    xlsx_path = os.path.join(CA_FOLDER, f"{base_name}.xlsx")
    
    data = read_xlsx(xlsx_path)
    data["case_num"] = case_num
    data["case_year"] = case_year
    
    print(f"\nInput file: {xlsx_path}")
    if os.path.exists(xlsx_path):
        print("Found existing Excel file.")
    else:
        print("No Excel file found. Creating new file...")
    
    data = prompt_for_missing(data)
    data = validate_and_prompt_conditional(data)
    write_xlsx(xlsx_path, data)
    print(f"Updated: {xlsx_path}")
    
    print(f"\nGenerating document (writer_judge={writer_judge})...")
    doc_data = generate_document(data, writer_judge=writer_judge)
    
    md_path = get_output_path(os.path.join(CA_FOLDER, f"{base_name}.md"))
    docx_path = get_output_path(os.path.join(CA_FOLDER, f"{base_name}.docx"))
    
    # Generate markdown
    md_content = generate_markdown(doc_data)
    # with open(md_path, 'w', encoding='utf-8') as f:
    #     f.write(md_content)
    # print(f"Created: {md_path}")
    
    # Generate DOCX
    try:
        generate_docx(doc_data, docx_path)
        print(f"Created: {docx_path}")
    except Exception as e:
        print(f"Warning: DOCX conversion failed: {e}")
    
    # Open documents in Microsoft Word
    print("\n[8] Opening documents in Microsoft Word...")
    import subprocess
    # try:
    #     subprocess.Popen(['start', 'winword', docx_path], shell=True)
    #     print(f"Opened: {docx_path}")
    # except Exception as e:
    #     print(f"Warning: Could not open generated judgment: {e}")
    
    hcal_doc_path = os.path.join(CA_FOLDER, f"{case_num}-{case_year}-hcal.doc")
    hcal_docx_path = os.path.join(CA_FOLDER, f"{case_num}-{case_year}-hcal.docx")
    
    hcal_path = None
    if os.path.exists(hcal_docx_path):
        hcal_path = hcal_docx_path
    elif os.path.exists(hcal_doc_path):
        hcal_path = hcal_doc_path
    
    # if hcal_path:
    #     try:
    #         subprocess.Popen(['start', 'winword', hcal_path], shell=True)
    #         print(f"Opened: {hcal_path}")
    #     except Exception as e:
    #         print(f"Warning: Could not open HCAL judgment: {e}")
    # else:
    #     print(f"HCAL judgment not found (expected {case_num}-{case_year}-hcal.doc or .docx)")
    
    print("\nDone!")


def main():
    print("=" * 60)
    print("CA Case Document Generator")
    print("=" * 60)
    
    case_num_input = input("\nEnter case number (e.g., 22 for HCAL22): ").strip()
    case_year_input = input("Enter case year (e.g., 2024): ").strip()
    writer_input = input(f"Enter writer_judge {sorted(WRITERS)} [default {DEFAULT_WRITER_JUDGE}]: ").strip() or DEFAULT_WRITER_JUDGE

    try:
        case_num = int(case_num_input)
        case_year = int(case_year_input)
        if case_year < 100:
            case_year = 2000 + case_year if case_year < 50 else 1900 + case_year
    except ValueError:
        print("Error: Case number and year must be integers")
        return

    run_generator(case_num, case_year, writer_judge=writer_input)


if __name__ == "__main__":
    main()
