from __future__ import annotations

import re
import posixpath
import xml.etree.ElementTree as ET
import zipfile
from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence
from xml.sax.saxutils import escape

from common.shared import (
    L2TP_PASSWORD,
    L2TP_PSK,
    L2TP_USER_PREFIX,
    OUTPUT_REGION_ORDER,
    SOCKS_PASSWORD,
    SOCKS_PORT,
    SOCKS_USER,
    RegionBuildResult,
    RegionPlan,
)
from common.credential_parser import _clean_mark_token
from common.ip_utils import sort_ipv4


L2TP_RESULT_HEADER = "入口IP/账号/密码/密钥/对应出口IP"
SOCKS5_RESULT_HEADER = "IP/端口/用户名/密码"
L2TP_OUTPUT_SEPARATOR = "====================================="
SOCKS5_OUTPUT_SEPARATOR = "======================================"


def parent_iface_for_index(index: int) -> str:
    return f"eth{index}"


def format_ip_txt(interfaces: Sequence[Dict[str, Any]]) -> str:
    """把网卡信息格式化成 huawei3.sh 需要的 ip.txt 内容。"""
    rows = []
    for item in sorted(interfaces, key=lambda data: sort_ipv4(data.get("private_ip_address") or "")):
        vlan_id = item.get("vlan_id")
        mac = item.get("mac_address") or ""
        private_ip = item.get("private_ip_address") or ""
        if vlan_id is None or not mac or not private_ip:
            continue
        parent_iface = str(item.get("_parent_iface") or "").strip()
        if parent_iface and parent_iface != parent_iface_for_index(0):
            rows.append(f"{parent_iface} {vlan_id} {mac} {private_ip}")
        else:
            rows.append(f"{vlan_id} {mac} {private_ip}")
    return "\n".join(rows) + ("\n" if rows else "")


def copy_output_footer() -> List[str]:
    """生成复制结果末尾的固定免责声明。"""
    return [
        L2TP_OUTPUT_SEPARATOR,
        "免责声明：仅用于合法合规的网络技术交流用途。",
        "使用者需自行承担一切行为责任。",
    ]


def result_output_footer(kind: str, mark: str = "") -> List[str]:
    """生成 L2TP/Socks5 结果框末尾的免责声明和用户标记。"""
    label = "Socks5" if str(kind or "").lower().startswith("socks") else "L2TP"
    separator = SOCKS5_OUTPUT_SEPARATOR if label == "Socks5" else L2TP_OUTPUT_SEPARATOR
    lines = [
        separator,
        f"{label}丨免责声明：仅用于合法合规的网络技术交流用途。",
        "使用者需自行承担一切行为责任。",
    ]
    mark_value = _clean_mark_token(mark)
    if mark_value:
        lines.append(mark_value)
    return lines


def append_result_footer(text: str, kind: str, mark: str = "") -> str:
    """给结果框文本追加指定类型的免责声明尾部。"""
    output = str(text or "").strip()
    if not output:
        return ""
    return "\n".join([output, *result_output_footer(kind, mark)])


def _looks_like_ipv4(value: str) -> bool:
    parts = str(value or "").split(".")
    if len(parts) != 4:
        return False
    try:
        return all(0 <= int(part) <= 255 for part in parts)
    except ValueError:
        return False


def extract_plain_socks5_output(text: str) -> str:
    """从当前 Socks5 结果框中提取纯 IP/端口/用户名/密码 行。"""
    rows: List[str] = []
    for line in str(text or "").replace("\r", "\n").splitlines():
        value = line.strip()
        if not value or value.startswith("="):
            continue
        parts = [part.strip() for part in value.split("/")]
        if len(parts) != 4:
            continue
        if not _looks_like_ipv4(parts[0]) or not re.fullmatch(r"\d{2,5}", parts[1]):
            continue
        if not parts[2] or not parts[3]:
            continue
        rows.append("/".join(parts[:4]))
    return "\n".join(rows)


def write_latest_result_files(base_dir: str | Path, *, l2tp: str = "", socks5: str = "", plain_socks5: str = "") -> None:
    """把最近一次结果落地保存，界面卡住时也能从文件取回。"""
    try:
        target = Path(base_dir)
        target.mkdir(parents=True, exist_ok=True)
        l2tp_text = str(l2tp or "").strip()
        socks5_text = str(socks5 or "").strip()
        plain_text = str(plain_socks5 or "").strip() or extract_plain_socks5_output(socks5_text).strip()
        (target / "last_l2tp.txt").write_text(l2tp_text + ("\n" if l2tp_text else ""), encoding="utf-8")
        (target / "last_socks5.txt").write_text(socks5_text + ("\n" if socks5_text else ""), encoding="utf-8")
        (target / "last_socks5_plain.txt").write_text(plain_text + ("\n" if plain_text else ""), encoding="utf-8")
    except Exception:
        pass


def migrate_latest_result_files(source_dir: str | Path, target_dir: str | Path) -> bool:
    """首次取得稳定账号身份后，把旧 AK/SK 目录中的结果迁移到账号目录。"""
    source = Path(source_dir)
    target = Path(target_dir)
    if source == target:
        return False
    target_l2tp_path = target / "last_l2tp.txt"
    target_socks_path = target / "last_socks5.txt"
    if target_l2tp_path.exists() and target_socks_path.exists():
        return False

    def read_text(base: Path, filename: str) -> str:
        try:
            return (base / filename).read_text(encoding="utf-8").strip()
        except Exception:
            return ""

    target_l2tp = read_text(target, "last_l2tp.txt")
    target_socks = read_text(target, "last_socks5.txt")
    source_l2tp = read_text(source, "last_l2tp.txt")
    source_socks = read_text(source, "last_socks5.txt")
    merged_l2tp = target_l2tp or source_l2tp
    merged_socks = target_socks or source_socks
    if (merged_l2tp, merged_socks) == (target_l2tp, target_socks):
        return False
    write_latest_result_files(target, l2tp=merged_l2tp, socks5=merged_socks)
    return True


def _xlsx_column_name(index: int) -> str:
    name = ""
    value = int(index)
    while value > 0:
        value, remainder = divmod(value - 1, 26)
        name = chr(65 + remainder) + name
    return name or "A"


def _xlsx_text(value: Any) -> str:
    return re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", "", str(value or ""))


def _xlsx_column_index(cell_ref: str) -> int:
    letters = re.sub(r"[^A-Za-z]", "", str(cell_ref or "")).upper()
    index = 0
    for char in letters:
        index = index * 26 + (ord(char) - 64)
    return index


def _xlsx_shared_strings(archive: zipfile.ZipFile) -> List[str]:
    if "xl/sharedStrings.xml" not in archive.namelist():
        return []
    try:
        root = ET.fromstring(archive.read("xl/sharedStrings.xml"))
    except Exception:
        return []
    values: List[str] = []
    for item in root.iter():
        if not item.tag.endswith("}si") and item.tag != "si":
            continue
        parts = [node.text or "" for node in item.iter() if node.tag.endswith("}t") or node.tag == "t"]
        values.append("".join(parts))
    return values


def _xlsx_cell_text(cell: ET.Element, shared_strings: Sequence[str]) -> str:
    cell_type = str(cell.attrib.get("t") or "")
    if cell_type == "inlineStr":
        return "".join(node.text or "" for node in cell.iter() if node.tag.endswith("}t") or node.tag == "t")
    value_node = next((node for node in cell.iter() if node.tag.endswith("}v") or node.tag == "v"), None)
    value = value_node.text if value_node is not None else ""
    if cell_type == "s":
        try:
            return str(shared_strings[int(str(value or "0"))])
        except Exception:
            return ""
    return str(value or "")


def _xlsx_header_key(value: Any) -> str:
    text = _xlsx_text(value).strip().lower()
    return re.sub(r"[\s_\-\/\\()（）:：]+", "", text)


def _xlsx_header_aliases(headers: Sequence[str]) -> List[set[str]]:
    aliases: List[set[str]] = [{_xlsx_header_key(header)} for header in headers]
    if len(aliases) >= 4:
        aliases[0].update(_xlsx_header_key(value) for value in ("日期时间", "时间", "time", "datetime", "date"))
        aliases[1].update(
            _xlsx_header_key(value)
            for value in (
                "用户名-简AK/SK",
                "用户名-简AK-SK",
                "用户名",
                "账号",
                "账号标识",
                "account",
                "mark",
                "mark(user)",
                "华为云用户名-简AK-SK",
                "天翼云用户名-简AK-SK",
                "火山云用户名-简AK-SK",
                "火山引擎用户名-简AK-SK",
            )
        )
        aliases[2].update(_xlsx_header_key(value) for value in ("地区", "地区(必填项)", "地域", "region"))
        aliases[3].update(_xlsx_header_key(value) for value in ("IP", "ip", "IP(必填项)", "公网IP", "旧IP"))
    return aliases


def _xlsx_find_header_row(raw_rows: Sequence[Sequence[str]], headers: Sequence[str]) -> tuple[int, List[int]]:
    aliases = _xlsx_header_aliases(headers)
    fallback = list(range(len(headers)))
    best_row = 0
    best_score = -1
    best_indexes = fallback
    for row_index, raw_row in enumerate(raw_rows[:8]):
        row_keys = [_xlsx_header_key(value) for value in raw_row]
        indexes: List[int] = []
        score = 0
        for fallback_index, alias_set in enumerate(aliases):
            match_index = next((index for index, key in enumerate(row_keys) if key in alias_set), -1)
            if match_index >= 0:
                indexes.append(match_index)
                score += 1
            else:
                indexes.append(fallback_index)
        if score > best_score:
            best_row = row_index
            best_score = score
            best_indexes = indexes
    return best_row, best_indexes


def _xlsx_display_width(value: Any) -> float:
    text = _xlsx_text(value)
    if not text:
        return 0.0
    max_width = 0.0
    for line in text.splitlines() or [""]:
        width = 0.0
        for char in line:
            width += 2.0 if ord(char) > 127 else 1.0
        max_width = max(max_width, width)
    return max_width


def _xlsx_used_ip_layout(headers: Sequence[str]) -> tuple[List[str], List[int], List[float]] | None:
    if len(headers) < 4:
        return None
    keys = [_xlsx_header_key(header) for header in headers[:4]]
    if keys[0] != _xlsx_header_key("日期时间") or keys[2] != _xlsx_header_key("地区") or keys[3] != _xlsx_header_key("IP"):
        return None
    if "aksk" not in keys[1] and "账号" not in keys[1] and "用户名" not in keys[1]:
        return None
    return (
        ["用户名-简AK/SK", "日期时间", "地区(必填项)", "IP(必填项)"],
        [1, 0, 2, 3],
        [21.5, 20.1, 13.2, 14.9],
    )


def _xlsx_auto_widths(
    headers: Sequence[str],
    rows: Sequence[Sequence[Any]],
    base_widths: Sequence[float],
    *,
    side_note: str = "",
) -> List[float]:
    widths = [float(width) for width in base_widths] or [18.0] * len(headers)
    while len(widths) < len(headers):
        widths.append(18.0)
    widths = widths[: len(headers)]
    for column_index, header in enumerate(headers):
        max_text_width = _xlsx_display_width(header)
        for row in rows:
            if column_index < len(row):
                max_text_width = max(max_text_width, _xlsx_display_width(row[column_index]))
        widths[column_index] = max(widths[column_index], min(max_text_width + 3.0, 58.0))
    note_width = _xlsx_display_width(side_note)
    if side_note and note_width + 4.0 > sum(widths):
        extra = min(note_width + 4.0 - sum(widths), 28.0)
        per_column = extra / max(len(widths), 1)
        widths = [width + per_column for width in widths]
    return [round(width, 1) for width in widths]


def _xlsx_worksheet_path(archive: zipfile.ZipFile, worksheet_name: str = "") -> str:
    worksheet_paths = [
        name
        for name in archive.namelist()
        if name.startswith("xl/worksheets/") and name.endswith(".xml")
    ]
    if not worksheet_paths:
        return ""
    requested_name = str(worksheet_name or "").strip()
    if not requested_name:
        return "xl/worksheets/sheet1.xml" if "xl/worksheets/sheet1.xml" in worksheet_paths else worksheet_paths[0]
    try:
        workbook_root = ET.fromstring(archive.read("xl/workbook.xml"))
        rels_root = ET.fromstring(archive.read("xl/_rels/workbook.xml.rels"))
        targets = {
            str(node.attrib.get("Id") or ""): str(node.attrib.get("Target") or "")
            for node in rels_root.iter()
            if node.tag.endswith("}Relationship") or node.tag == "Relationship"
        }
        for node in workbook_root.iter():
            if not (node.tag.endswith("}sheet") or node.tag == "sheet"):
                continue
            if str(node.attrib.get("name") or "").strip() != requested_name:
                continue
            relationship_id = str(
                node.attrib.get("{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id")
                or node.attrib.get("r:id")
                or ""
            )
            target = targets.get(relationship_id, "").replace("\\", "/")
            if not target:
                return ""
            candidate = target.lstrip("/") if target.startswith("/") else posixpath.normpath(posixpath.join("xl", target))
            return candidate if candidate in worksheet_paths else ""
    except Exception:
        return ""
    return ""


def read_excel_table_rows(
    path: str | Path,
    headers: Sequence[str],
    *,
    worksheet_name: str = "",
) -> List[Dict[str, str]]:
    """读取软件生成或用户用 WPS/Excel 保存过的旧 IP 库 xlsx。"""
    expected_headers = [str(header or "") for header in headers]
    if not expected_headers:
        return []
    try:
        with zipfile.ZipFile(Path(path), "r") as archive:
            worksheet_path = _xlsx_worksheet_path(archive, worksheet_name)
            if not worksheet_path:
                return []
            shared_strings = _xlsx_shared_strings(archive)
            root = ET.fromstring(archive.read(worksheet_path))
    except Exception:
        return []

    raw_rows: List[List[str]] = []
    for row in root.iter():
        if not row.tag.endswith("}row") and row.tag != "row":
            continue
        values: List[str] = []
        for cell in row:
            if not cell.tag.endswith("}c") and cell.tag != "c":
                continue
            column_index = _xlsx_column_index(cell.attrib.get("r", ""))
            if column_index < 1:
                column_index = len(values) + 1
            while len(values) < column_index - 1:
                values.append("")
            values.append(_xlsx_cell_text(cell, shared_strings))
        if values:
            raw_rows.append(values)
    if not raw_rows:
        return []

    header_row_index, column_indexes = _xlsx_find_header_row(raw_rows, expected_headers)

    rows: List[Dict[str, str]] = []
    for raw_row in raw_rows[header_row_index + 1:]:
        item = {
            header: (str(raw_row[index]).strip() if index < len(raw_row) else "")
            for header, index in zip(expected_headers, column_indexes)
        }
        if any(item.values()):
            rows.append(item)
    return rows


def write_excel_view_file(
    path: str | Path,
    headers: Sequence[str],
    rows: Sequence[Dict[str, Any]],
    *,
    column_widths: Sequence[float] = (),
    target_path: str | Path | None = None,
    side_note: str = "",
    extra_sheets: Sequence[Dict[str, Any]] = (),
    raise_errors: bool = False,
) -> Optional[Path]:
    """生成一个带合适列宽的 Excel 文件。默认写到 CSV 同名 xlsx。"""
    try:
        target = Path(target_path) if target_path is not None else Path(path).with_suffix(".xlsx")
        def build_sheet_xml(
            sheet_headers: Sequence[str],
            sheet_rows: Sequence[Dict[str, Any]],
            sheet_column_widths: Sequence[float],
            sheet_side_note: str,
        ) -> str:
            normalized_headers = [str(header or "") for header in sheet_headers]
            side_note_text = _xlsx_text(sheet_side_note).strip()
            layout = _xlsx_used_ip_layout(normalized_headers)
            if layout is not None:
                display_headers, column_order, base_widths = layout
                data_rows = [
                    [row.get(normalized_headers[index], "") for index in column_order]
                    for row in sheet_rows
                ]
            else:
                display_headers = normalized_headers
                data_rows = [[row.get(header, "") for header in normalized_headers] for row in sheet_rows]
                base_widths = list(sheet_column_widths) or [22.0, 34.0, 14.0, 18.0]
            widths = _xlsx_auto_widths(display_headers, data_rows, base_widths, side_note=side_note_text)
            sheet_parts = [
                '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>',
                '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">',
                f'<dimension ref="A1:{_xlsx_column_name(len(display_headers))}{max(len(data_rows) + (3 if side_note_text else 1), 1)}"/>',
                '<sheetFormatPr defaultRowHeight="18"/>',
                "<cols>",
            ]
            for index, width in enumerate(widths, start=1):
                sheet_parts.append(f'<col min="{index}" max="{index}" width="{float(width):.1f}" customWidth="1"/>')
            sheet_parts.append("</cols><sheetData>")
            if side_note_text:
                all_rows: List[List[Any]] = [[side_note_text], [], display_headers, *data_rows]
                table_start_row = 3
            else:
                all_rows = [display_headers, *data_rows]
                table_start_row = 1
            for row_index, values in enumerate(all_rows, start=1):
                row_attrs = f' r="{row_index}"'
                if side_note_text and row_index in {1, 2}:
                    row_attrs += ' ht="24" customHeight="1"'
                sheet_parts.append(f"<row{row_attrs}>")
                for column_index, value in enumerate(values, start=1):
                    ref = f"{_xlsx_column_name(column_index)}{row_index}"
                    style_id = "1" if side_note_text and row_index == 1 and column_index == 1 else "2"
                    sheet_parts.append(f'<c r="{ref}" s="{style_id}" t="inlineStr"><is><t>{escape(_xlsx_text(value))}</t></is></c>')
                sheet_parts.append("</row>")
            sheet_parts.append("</sheetData>")
            if side_note_text:
                note_range = f"A1:{_xlsx_column_name(len(display_headers))}{table_start_row - 1}"
                sheet_parts.append(f'<mergeCells count="1"><mergeCell ref="{note_range}"/></mergeCells>')
            sheet_parts.append("</worksheet>")
            return "".join(sheet_parts)

        sheet_specs: List[Dict[str, Any]] = [
            {
                "name": "旧IP库",
                "headers": list(headers),
                "rows": list(rows),
                "column_widths": list(column_widths),
                "side_note": side_note,
            }
        ]
        used_sheet_names = {"旧IP库"}
        for index, extra_sheet in enumerate(extra_sheets, start=2):
            raw_name = str(extra_sheet.get("name") or f"工作表{index}").strip()
            sheet_name = re.sub(r"[\\/*?:\[\]]", "_", raw_name).strip("'")[:31] or f"工作表{index}"
            while sheet_name in used_sheet_names:
                suffix = f"_{index}"
                sheet_name = f"{sheet_name[:31 - len(suffix)]}{suffix}"
            used_sheet_names.add(sheet_name)
            sheet_specs.append(
                {
                    "name": sheet_name,
                    "headers": list(extra_sheet.get("headers") or headers),
                    "rows": list(extra_sheet.get("rows") or []),
                    "column_widths": list(extra_sheet.get("column_widths") or column_widths),
                    "side_note": str(extra_sheet.get("side_note") or ""),
                }
            )
        sheet_xml = [
            build_sheet_xml(
                spec["headers"],
                spec["rows"],
                spec["column_widths"],
                spec["side_note"],
            )
            for spec in sheet_specs
        ]

        content_type_overrides = "\n".join(
            f'  <Override PartName="/xl/worksheets/sheet{index}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>'
            for index in range(1, len(sheet_specs) + 1)
        )
        content_types = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
  <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
  <Default Extension="xml" ContentType="application/xml"/>
  <Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
{content_type_overrides}
  <Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
</Types>"""
        root_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
</Relationships>"""
        workbook_sheets = "\n".join(
            f'    <sheet name="{escape(spec["name"])}" sheetId="{index}" r:id="rId{index}"/>'
            for index, spec in enumerate(sheet_specs, start=1)
        )
        workbook = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
  <sheets>
{workbook_sheets}
  </sheets>
</workbook>"""
        worksheet_relationships = "\n".join(
            f'  <Relationship Id="rId{index}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet{index}.xml"/>'
            for index in range(1, len(sheet_specs) + 1)
        )
        styles_relationship_id = len(sheet_specs) + 1
        workbook_rels = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
{worksheet_relationships}
  <Relationship Id="rId{styles_relationship_id}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
</Relationships>"""
        styles = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
  <fonts count="1"><font><sz val="11"/><name val="宋体"/></font></fonts>
  <fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>
  <borders count="2">
    <border><left/><right/><top/><bottom/><diagonal/></border>
    <border>
      <left style="thin"><color auto="1"/></left>
      <right style="thin"><color auto="1"/></right>
      <top style="thin"><color auto="1"/></top>
      <bottom style="thin"><color auto="1"/></bottom>
      <diagonal/>
    </border>
  </borders>
  <cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
  <cellXfs count="3">
    <xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>
    <xf numFmtId="0" fontId="0" fillId="0" borderId="1" xfId="0" applyBorder="1" applyAlignment="1">
      <alignment horizontal="left" vertical="center" wrapText="1"/>
    </xf>
    <xf numFmtId="0" fontId="0" fillId="0" borderId="1" xfId="0" applyBorder="1" applyAlignment="1">
      <alignment horizontal="left" vertical="center"/>
    </xf>
  </cellXfs>
  <cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>
</styleSheet>"""

        target.parent.mkdir(parents=True, exist_ok=True)
        with zipfile.ZipFile(target, "w", compression=zipfile.ZIP_DEFLATED) as archive:
            archive.writestr("[Content_Types].xml", content_types)
            archive.writestr("_rels/.rels", root_rels)
            archive.writestr("xl/workbook.xml", workbook)
            archive.writestr("xl/_rels/workbook.xml.rels", workbook_rels)
            archive.writestr("xl/styles.xml", styles)
            for index, worksheet_xml in enumerate(sheet_xml, start=1):
                archive.writestr(f"xl/worksheets/sheet{index}.xml", worksheet_xml)
        return target
    except Exception:
        if raise_errors:
            raise
        return None


def output_region_label(plan: RegionPlan) -> str:
    """返回结果输出中展示给用户看的地区名称。"""
    return plan.label


def _result_connection_value(result: RegionBuildResult, attr: str, default: object) -> str:
    value = str(getattr(result, attr, "") or "").strip()
    return value or str(default)


def format_l2tp_block(result: RegionBuildResult) -> str:
    """生成单个地区的 L2TP 结果块。"""
    entry_ip = result.server_ip or (result.public_ips[0] if result.public_ips else "")
    l2tp_password = _result_connection_value(result, "l2tp_password", L2TP_PASSWORD)
    l2tp_psk = _result_connection_value(result, "l2tp_psk", L2TP_PSK)
    lines = [
        output_region_label(result.plan),
        L2TP_RESULT_HEADER,
    ]
    for index, ip in enumerate(result.public_ips, start=1):
        lines.append(f"{entry_ip}/{L2TP_USER_PREFIX}{index}/{l2tp_password}/{l2tp_psk}/{ip}")
    return "\n".join(lines)


def format_socks5_block(result: RegionBuildResult) -> str:
    """生成单个地区的 SOCKS5 结果块。"""
    socks_port = _result_connection_value(result, "socks_port", SOCKS_PORT)
    socks_user = _result_connection_value(result, "socks_user", SOCKS_USER)
    socks_password = _result_connection_value(result, "socks_password", SOCKS_PASSWORD)
    socks5_lines = [f"{ip}/{socks_port}/{socks_user}/{socks_password}" for ip in result.public_ips if ip]
    return "\n".join(
        [
            output_region_label(result.plan),
            SOCKS5_RESULT_HEADER,
            *socks5_lines,
        ]
    )


def format_result_block(result: RegionBuildResult) -> str:
    """默认结果展示使用 SOCKS5 格式，保留旧函数名兼容原调用。"""
    return format_socks5_block(result)


def ordered_results(results: Sequence[RegionBuildResult]) -> List[RegionBuildResult]:
    """按旧版本约定的地区顺序排列输出结果。"""
    order = {key: index for index, key in enumerate(OUTPUT_REGION_ORDER)}
    return sorted(results, key=lambda item: order.get(item.plan.key, 999))


def format_l2tp_output(results: Sequence[RegionBuildResult]) -> str:
    """生成可复制的完整 L2TP 输出文本。"""
    blocks = [format_l2tp_block(result) for result in ordered_results(results)]
    if not blocks:
        return ""
    return append_result_footer("\n".join(blocks), "L2TP")


def format_socks5_output(results: Sequence[RegionBuildResult]) -> str:
    """生成可复制的完整 SOCKS5 输出文本。"""
    blocks = [format_socks5_block(result) for result in ordered_results(results)]
    if not blocks:
        return ""
    return append_result_footer("\n".join(blocks), "Socks5")


def format_plain_socks5_output(results: Sequence[RegionBuildResult]) -> str:
    """生成不带地区标题和页脚的纯 SOCKS5 列表。"""
    rows: List[str] = []
    for result in ordered_results(results):
        socks_port = _result_connection_value(result, "socks_port", SOCKS_PORT)
        socks_user = _result_connection_value(result, "socks_user", SOCKS_USER)
        socks_password = _result_connection_value(result, "socks_password", SOCKS_PASSWORD)
        for ip in result.public_ips:
            if ip:
                rows.append(f"{ip}/{socks_port}/{socks_user}/{socks_password}")
    return "\n".join(rows)
