"""Dynamic master data: companies, bank accounts, aliases and audit trail.

Companies, cashier logins and bank accounts live in the database instead of
being hard-coded in the UI. Bank account numbers are stored normalized
(digits only) under a database UNIQUE constraint, so two concurrent
submissions can never produce two usable accounts for the same number.
Company-side registrations are requests: only administrator-approved
(``active``) accounts inside their effective interval may identify ownership,
accept uploads or take part in coverage calculation. Every change is recorded
in ``master_data_changes`` with before/after values, actor, time and reason.
"""

from __future__ import annotations

from datetime import date, datetime, timezone
import json
import re
import sqlite3

from .db import utc_now


ACCOUNT_TYPES = ("基本户", "一般户", "专用户")
ACCOUNT_STATUSES = ("pending", "active", "returned", "disabled")
ALIAS_KINDS = ("name", "account")

# Deterministic alias match priority: an exact account-number hit always
# beats an account alias, which always beats a name alias. The per-alias
# ``priority`` column only orders matches within the same tier.
PRIORITY_EXACT_ACCOUNT = 0
PRIORITY_ACCOUNT_ALIAS = 1000
PRIORITY_NAME_ALIAS = 2000

_ACCOUNT_NUMBER_PATTERN = re.compile(r"^[0-9]{6,32}$")
_DATE_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}$")
_FULL_WIDTH_DIGITS = str.maketrans("０１２３４５６７８９", "0123456789")


class ConflictError(ValueError):
    """A uniqueness or ownership conflict (mapped to HTTP 409)."""


def utc_today() -> str:
    return datetime.now(timezone.utc).date().isoformat()


def normalize_account_number(raw: object) -> str:
    """Normalize an account number to digits only; raise ValueError if invalid.

    Spaces, dashes and full-width digits entered by cashiers or emitted by
    bank exports collapse to one canonical form, which is what the UNIQUE
    constraint and all matching operate on.
    """
    text = str(raw or "").translate(_FULL_WIDTH_DIGITS)
    text = re.sub(r"[\s\-‐-‒–—]+", "", text)
    if not _ACCOUNT_NUMBER_PATTERN.fullmatch(text):
        raise ValueError("银行账号须为 6-32 位数字（可含空格或短横线分隔）。")
    return text


def mask_account_number(number: str) -> str:
    """Masked display form; the full number stays server-side."""
    if len(number) <= 4:
        return f"****{number}"
    return f"****{number[-4:]}"


def validate_date(value: object, field: str, *, required: bool = False) -> str | None:
    text = str(value or "").strip()
    if not text:
        if required:
            raise ValueError(f"{field}不能为空。")
        return None
    if not _DATE_PATTERN.fullmatch(text):
        raise ValueError(f"{field}须为 YYYY-MM-DD 格式。")
    try:
        date.fromisoformat(text)
    except ValueError:
        raise ValueError(f"{field}不是有效日期。") from None
    return text


def validate_account_type(value: object) -> str:
    text = str(value or "").strip() or "一般户"
    if text not in ACCOUNT_TYPES:
        raise ValueError(f"账户类型必须是：{'、'.join(ACCOUNT_TYPES)}。")
    return text


def _snapshot(row: sqlite3.Row | None) -> dict[str, object] | None:
    if row is None:
        return None
    return {key: row[key] for key in row.keys()}


def record_change(
    connection: sqlite3.Connection,
    entity_type: str,
    entity_id: int,
    action: str,
    before: dict[str, object] | None,
    after: dict[str, object] | None,
    reason: str | None,
    actor: sqlite3.Row | None,
) -> None:
    """Append a before/after audit entry for a master data change."""
    connection.execute(
        """
        INSERT INTO master_data_changes (
            entity_type, entity_id, action, before_json, after_json,
            reason, actor_user_id, actor_username, created_at
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
        """,
        (
            entity_type,
            entity_id,
            action,
            json.dumps(before, ensure_ascii=False) if before is not None else None,
            json.dumps(after, ensure_ascii=False) if after is not None else None,
            reason,
            actor["id"] if actor is not None else None,
            actor["username"] if actor is not None else None,
            utc_now(),
        ),
    )


# ---------------------------------------------------------------------------
# Companies
# ---------------------------------------------------------------------------


def create_company(
    connection: sqlite3.Connection,
    name: str,
    credit_code: str | None,
    cashier_name: str | None,
    actor: sqlite3.Row | None,
) -> int:
    """Create a company master record; returns the new id."""
    name = name.strip()
    if not name:
        raise ValueError("公司名称不能为空。")
    now = utc_now()
    try:
        with connection:
            cursor = connection.execute(
                """
                INSERT INTO companies (
                    name, credit_code, cashier_name, status, created_at, updated_at
                ) VALUES (?, ?, ?, 'active', ?, ?)
                """,
                (name, (credit_code or "").strip() or None,
                 (cashier_name or "").strip() or None, now, now),
            )
    except sqlite3.IntegrityError as exc:
        raise ConflictError("公司名称已存在。") from exc
    company_id = int(cursor.lastrowid)
    with connection:
        record_change(
            connection, "company", company_id, "create",
            None, {"name": name, "credit_code": credit_code or None,
                   "cashier_name": cashier_name or None, "status": "active"},
            None, actor,
        )
    return company_id


# ---------------------------------------------------------------------------
# Bank accounts
# ---------------------------------------------------------------------------


def get_account(connection: sqlite3.Connection, account_id: int) -> sqlite3.Row | None:
    return connection.execute(
        "SELECT * FROM bank_accounts WHERE id = ?", (account_id,)
    ).fetchone()


def submit_bank_account(
    connection: sqlite3.Connection,
    *,
    company_id: int,
    bank_name: str,
    account_type: object,
    account_number: object,
    account_name: object = None,
    start_date: object = None,
    actor: sqlite3.Row | None,
) -> sqlite3.Row:
    """Register an account for review; returns the resulting account row.

    A normalized number already present is never duplicated: resubmitting a
    returned request from the same company reopens that same row as pending;
    any other existing row is a conflict. The UNIQUE constraint on
    ``account_number`` is the final guard for concurrent submissions.
    """
    number = normalize_account_number(account_number)
    bank = str(bank_name or "").strip()
    if not bank:
        raise ValueError("开户银行不能为空。")
    kind = validate_account_type(account_type)
    holder = str(account_name or "").strip() or None
    requested_from = validate_date(start_date, "启用日期")
    now = utc_now()

    existing = connection.execute(
        "SELECT * FROM bank_accounts WHERE account_number = ?", (number,)
    ).fetchone()

    if existing is None:
        try:
            with connection:
                cursor = connection.execute(
                    """
                    INSERT INTO bank_accounts (
                        company_id, account_number, account_name, bank_name,
                        account_type, status, effective_from, submitted_by,
                        created_at, updated_at
                    ) VALUES (?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?)
                    """,
                    (company_id, number, holder, bank, kind,
                     requested_from, actor["id"] if actor else None, now, now),
                )
        except sqlite3.IntegrityError as exc:
            # Lost a concurrent-insert race on the UNIQUE constraint.
            raise ConflictError("该银行账号已登记，请等待现有申请处理。") from exc
        account_id = int(cursor.lastrowid)
        with connection:
            record_change(
                connection, "bank_account", account_id, "submit", None,
                {"company_id": company_id, "account_number": number,
                 "bank_name": bank, "account_type": kind, "status": "pending",
                 "effective_from": requested_from},
                None, actor,
            )
        return get_account(connection, account_id)

    if existing["status"] == "returned" and existing["company_id"] == company_id:
        before = _snapshot(existing)
        with connection:
            connection.execute(
                """
                UPDATE bank_accounts
                SET account_name = ?, bank_name = ?, account_type = ?,
                    status = 'pending', effective_from = ?,
                    submitted_by = ?, reviewed_by = NULL, reviewed_at = NULL,
                    review_reason = NULL, updated_at = ?
                WHERE id = ? AND status = 'returned'
                """,
                (holder, bank, kind, requested_from,
                 actor["id"] if actor else None, now, existing["id"]),
            )
            record_change(
                connection, "bank_account", existing["id"], "resubmit",
                before, {"account_number": number, "bank_name": bank,
                         "account_type": kind, "status": "pending",
                         "effective_from": requested_from},
                "退回后重新提交", actor,
            )
        return get_account(connection, existing["id"])

    if existing["company_id"] == company_id:
        raise ConflictError("该银行账号已登记，请等待现有申请处理。")
    raise ConflictError("该银行账号已被其他公司登记，请联系管理员核对。")


def review_bank_account(
    connection: sqlite3.Connection,
    account_id: int,
    decision: str,
    reason: str | None,
    actor: sqlite3.Row,
    *,
    effective_from: object = None,
    effective_to: object = None,
) -> sqlite3.Row:
    """Approve, return or disable a registration; returns the updated row."""
    account = get_account(connection, account_id)
    if account is None:
        raise LookupError("账户不存在。")
    reason = (reason or "").strip() or None
    today = utc_today()
    before = _snapshot(account)

    if decision == "approve":
        if account["status"] != "pending":
            raise ConflictError("只有待复核的账户可以审核通过。")
        start = validate_date(effective_from, "启用日期") or account["effective_from"] or today
        with connection:
            connection.execute(
                """
                UPDATE bank_accounts
                SET status = 'active', effective_from = ?, effective_to = NULL,
                    reviewed_by = ?, reviewed_at = ?, review_reason = ?, updated_at = ?
                WHERE id = ?
                """,
                (start, actor["id"], utc_now(), reason, utc_now(), account_id),
            )
            record_change(
                connection, "bank_account", account_id, "approve", before,
                {"status": "active", "effective_from": start}, reason, actor,
            )
    elif decision == "return":
        if account["status"] != "pending":
            raise ConflictError("只有待复核的账户可以退回。")
        if reason is None:
            raise ValueError("退回必须填写原因。")
        with connection:
            connection.execute(
                """
                UPDATE bank_accounts
                SET status = 'returned', reviewed_by = ?, reviewed_at = ?,
                    review_reason = ?, updated_at = ?
                WHERE id = ?
                """,
                (actor["id"], utc_now(), reason, utc_now(), account_id),
            )
            record_change(
                connection, "bank_account", account_id, "return", before,
                {"status": "returned"}, reason, actor,
            )
    elif decision == "disable":
        if account["status"] != "active":
            raise ConflictError("只有已启用的账户可以停用。")
        if reason is None:
            raise ValueError("停用必须填写原因。")
        end = validate_date(effective_to, "停用日期") or today
        with connection:
            connection.execute(
                """
                UPDATE bank_accounts
                SET status = 'disabled', effective_to = ?,
                    reviewed_by = ?, reviewed_at = ?, review_reason = ?, updated_at = ?
                WHERE id = ?
                """,
                (end, actor["id"], utc_now(), reason, utc_now(), account_id),
            )
            record_change(
                connection, "bank_account", account_id, "disable", before,
                {"status": "disabled", "effective_to": end}, reason, actor,
            )
    else:
        raise ValueError("审核决定必须是 approve、return 或 disable。")
    return get_account(connection, account_id)


def in_effective_window(account: sqlite3.Row, on_date: str) -> bool:
    """True when ``on_date`` falls inside the account's effective interval.

    ``effective_to`` is the last participating day (inclusive).
    """
    if account["effective_from"] and on_date < account["effective_from"]:
        return False
    if account["effective_to"] and on_date > account["effective_to"]:
        return False
    return True


def is_usable(account: sqlite3.Row, on_date: str) -> bool:
    """True when the account may accept uploads / be selected on a date."""
    return account["status"] == "active" and in_effective_window(account, on_date)


def is_identifiable(account: sqlite3.Row, on_date: str) -> bool:
    """True when the account may resolve ownership for a transaction date.

    A disabled account keeps identifying historical rows inside its effective
    window — disabling stops future participation, it never rewrites history.
    """
    return account["status"] in ("active", "disabled") and in_effective_window(
        account, on_date
    )


def list_accounts(
    connection: sqlite3.Connection,
    company_id: int | None = None,
    status: str | None = None,
) -> list[sqlite3.Row]:
    conditions: list[str] = []
    params: list[object] = []
    if company_id is not None:
        conditions.append("a.company_id = ?")
        params.append(company_id)
    if status is not None:
        if status not in ACCOUNT_STATUSES:
            raise ValueError("无效的账户状态。")
        conditions.append("a.status = ?")
        params.append(status)
    where = f"WHERE {' AND '.join(conditions)}" if conditions else ""
    return connection.execute(
        f"""
        SELECT a.*, c.name AS company_name
        FROM bank_accounts a
        JOIN companies c ON c.id = a.company_id
        {where}
        ORDER BY a.id
        """,
        params,
    ).fetchall()


def usable_accounts(
    connection: sqlite3.Connection, company_id: int, on_date: str | None = None
) -> list[sqlite3.Row]:
    """Accounts allowed to upload / identify ownership for the company today."""
    day = on_date or utc_today()
    return [
        account
        for account in list_accounts(connection, company_id=company_id)
        if is_usable(account, day)
    ]


# ---------------------------------------------------------------------------
# Aliases
# ---------------------------------------------------------------------------


def add_alias(
    connection: sqlite3.Connection,
    account_id: int,
    alias_kind: str,
    alias_value: object,
    priority: object = None,
    effective_from: object = None,
    effective_to: object = None,
    actor: sqlite3.Row | None = None,
) -> int:
    """Attach a name/account alias with an effective interval; returns the id."""
    if get_account(connection, account_id) is None:
        raise LookupError("账户不存在。")
    if alias_kind not in ALIAS_KINDS:
        raise ValueError("别名类型必须是 name 或 account。")
    if alias_kind == "account":
        value = normalize_account_number(alias_value)
    else:
        value = re.sub(r"\s+", "", str(alias_value or ""))
        if not value:
            raise ValueError("户名别名不能为空。")
    try:
        rank = int(priority) if priority not in (None, "") else 100
    except (TypeError, ValueError):
        raise ValueError("优先级必须是整数。") from None
    start = validate_date(effective_from, "别名生效日期")
    end = validate_date(effective_to, "别名失效日期")
    if start and end and end < start:
        raise ValueError("别名失效日期不能早于生效日期。")
    try:
        with connection:
            cursor = connection.execute(
                """
                INSERT INTO account_aliases (
                    bank_account_id, alias_kind, alias_value, priority,
                    effective_from, effective_to, created_by, created_at
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
                """,
                (account_id, alias_kind, value, rank, start, end,
                 actor["id"] if actor else None, utc_now()),
            )
    except sqlite3.IntegrityError as exc:
        raise ConflictError("该账户下相同别名已存在。") from exc
    alias_id = int(cursor.lastrowid)
    with connection:
        record_change(
            connection, "account_alias", alias_id, "create", None,
            {"bank_account_id": account_id, "alias_kind": alias_kind,
             "alias_value": value, "priority": rank,
             "effective_from": start, "effective_to": end},
            None, actor,
        )
    return alias_id


def list_aliases(connection: sqlite3.Connection, account_id: int) -> list[sqlite3.Row]:
    return connection.execute(
        "SELECT * FROM account_aliases WHERE bank_account_id = ? ORDER BY id",
        (account_id,),
    ).fetchall()


def match_account(
    connection: sqlite3.Connection,
    *,
    account_number: object = None,
    name: object = None,
    on_date: str | None = None,
) -> list[dict[str, object]]:
    """Resolve an observed counterparty to operating, in-window accounts.

    Deterministic priority: exact account number < account alias < name
    alias; within one tier the alias ``priority`` column orders the hits.
    Pending and returned accounts never match; disabled accounts still match
    transaction dates inside their effective window (see ``is_identifiable``).
    """
    day = on_date or utc_today()
    hits: list[tuple[int, sqlite3.Row, str]] = []

    number = str(account_number or "").strip()
    if number:
        try:
            normalized = normalize_account_number(number)
        except ValueError:
            normalized = None
        if normalized is not None:
            exact = connection.execute(
                "SELECT * FROM bank_accounts WHERE account_number = ?",
                (normalized,),
            ).fetchone()
            if exact is not None and is_identifiable(exact, day):
                hits.append((PRIORITY_EXACT_ACCOUNT, exact, "account_exact"))
            alias_rows = connection.execute(
                """
                SELECT a.*, al.priority AS alias_priority,
                       al.effective_from AS alias_from, al.effective_to AS alias_to
                FROM account_aliases al
                JOIN bank_accounts a ON a.id = al.bank_account_id
                WHERE al.alias_kind = 'account' AND al.alias_value = ?
                """,
                (normalized,),
            ).fetchall()
            for alias in alias_rows:
                if _alias_in_window(alias, day) and is_identifiable(alias, day):
                    hits.append(
                        (PRIORITY_ACCOUNT_ALIAS + alias["alias_priority"], alias, "account_alias")
                    )

    normalized_name = re.sub(r"\s+", "", str(name or ""))
    if normalized_name:
        alias_rows = connection.execute(
            """
            SELECT a.*, al.priority AS alias_priority,
                   al.effective_from AS alias_from, al.effective_to AS alias_to
            FROM account_aliases al
            JOIN bank_accounts a ON a.id = al.bank_account_id
            WHERE al.alias_kind = 'name' AND al.alias_value = ?
            """,
            (normalized_name,),
        ).fetchall()
        for alias in alias_rows:
            if _alias_in_window(alias, day) and is_identifiable(alias, day):
                hits.append(
                    (PRIORITY_NAME_ALIAS + alias["alias_priority"], alias, "name_alias")
                )

    hits.sort(key=lambda item: (item[0], item[1]["id"]))
    seen: set[int] = set()
    results: list[dict[str, object]] = []
    for rank, account, via in hits:
        if account["id"] in seen:
            continue
        seen.add(account["id"])
        results.append(
            {
                "bank_account_id": account["id"],
                "company_id": account["company_id"],
                "via": via,
                "priority": rank,
            }
        )
    return results


def _alias_in_window(alias: sqlite3.Row, on_date: str) -> bool:
    if alias["alias_from"] and on_date < alias["alias_from"]:
        return False
    if alias["alias_to"] and on_date > alias["alias_to"]:
        return False
    return True
