from __future__ import annotations

from typing import Literal

from pydantic import BaseModel, ConfigDict, Field


class StrictModel(BaseModel):
    model_config = ConfigDict(extra="forbid")


class DataSourceItem(StrictModel):
    source_key: str
    label: str
    available: bool
    configured: bool
    ingestion_supported: bool
    activity_support: bool
    valuation_support: bool
    capabilities: list[str]
    record_count: int
    last_successful_confirmed_at: str | None
    last_attempt_at: str | None
    last_attempt_status: str | None
    known_gaps: list[str]


class DataSourcesResponse(StrictModel):
    sources: list[DataSourceItem]
    data_cutoff: str
    input_fingerprint: str


class PostFinanceInstrumentDecision(StrictModel):
    source_symbol: str = Field(min_length=1, max_length=32)
    instrument_name: str = Field(min_length=1, max_length=255)
    isin: str = Field(pattern=r"^[A-Z]{2}[A-Z0-9]{9}[0-9]$")
    asset_class: Literal["stock", "etf"]
    source_currency: str = Field(pattern=r"^[A-Z]{3}$")
    source_venue: Literal["unknown"] = "unknown"
    provider: Literal["fmp"]
    valuation_symbol: str = Field(min_length=1, max_length=64)
    valuation_venue: str = Field(min_length=1, max_length=32)
    valuation_currency: str = Field(pattern=r"^[A-Z]{3}$")


class OwnerConfirmedManualPlaceholderResolution(StrictModel):
    manual_transaction_id: str = Field(min_length=1, max_length=255)
    expected_source_account_id: str = Field(min_length=1, max_length=255)
    target_account_id: str = Field(min_length=1, max_length=255)
    expected_isin: str = Field(pattern=r"^[A-Z]{2}[A-Z0-9]{9}[0-9]$")
    expected_quantity: str = Field(min_length=1, max_length=64)
    reason_code: Literal["manual_placeholder_wrong_account_confirmed_by_owner"]
    owner_confirmed_at: str = Field(min_length=1, max_length=64)
    owner_confirmation_note: str = Field(min_length=20, max_length=1000)


class IngestionPreviewRequest(StrictModel):
    source_key: Literal["canonical_transactions", "legacy_account_values", "cash_account_snapshots", "postfinance_etrading"]
    scope_kind: Literal["portfolio", "account"] = "portfolio"
    account_id: str | None = None
    period_from: str
    period_to: str
    data_cutoff: str | None = None
    file_name: str | None = Field(default=None, max_length=255)
    content_base64: str | None = Field(default=None, max_length=2_800_000)
    instrument_mappings: list[PostFinanceInstrumentDecision] = Field(default_factory=list, max_length=100)
    allow_manual_baseline_supersession: bool = False
    owner_confirmed_manual_placeholder_resolutions: list[OwnerConfirmedManualPlaceholderResolution] = Field(
        default_factory=list, max_length=10
    )


class IngestionCounts(StrictModel):
    discovered: int
    new: int
    unchanged: int
    duplicate: int
    ambiguous: int
    blocked: int
    versioned: int


class QualityImpact(StrictModel):
    status: Literal["complete", "partial", "unavailable"]
    reason_codes: list[str]


class PreviewItem(StrictModel):
    source_record_ref: str
    record_kind: Literal["activity", "valuation"]
    disposition: Literal["new", "unchanged", "duplicate", "ambiguous", "blocked", "versioned"]
    account_ref: str
    account_label: str
    as_of: str
    summary: dict[str, object]


class IngestionPreviewResponse(StrictModel):
    source_key: str
    scope_kind: Literal["portfolio", "account"]
    account_id: str | None
    period_from: str
    period_to: str
    data_cutoff: str
    preview_id: str
    confirmation_id: str
    preview_created_at: str
    expires_at: str
    source_revision: str
    input_fingerprint: str
    payload_hash: str
    counts: IngestionCounts
    planned_activities: list[dict[str, object]]
    planned_valuations: list[dict[str, object]]
    planned_corrections: list[dict[str, object]]
    quality_impact: QualityImpact
    items: list[PreviewItem]
    truncated: bool
    document_type: str | None = None
    file_hash: str | None = None
    operation_counts: dict[str, int] | None = None
    baseline_only: bool | None = None
    expected_changes: dict[str, int] | None = None
    performance_impact: dict[str, object] | None = None
    cash_reconciliation: dict[str, object] | None = None


class IngestionConfirmRequest(StrictModel):
    source_key: Literal["canonical_transactions", "legacy_account_values", "cash_account_snapshots", "postfinance_etrading"]
    scope_kind: Literal["portfolio", "account"] = "portfolio"
    account_id: str | None = None
    period_from: str
    period_to: str
    data_cutoff: str
    preview_id: str
    confirmation_id: str
    preview_created_at: str
    source_revision: str
    input_fingerprint: str
    payload_hash: str
    confirm: Literal[True]
    file_name: str | None = Field(default=None, max_length=255)
    content_base64: str | None = Field(default=None, max_length=2_800_000)
    instrument_mappings: list[PostFinanceInstrumentDecision] = Field(default_factory=list, max_length=100)
    allow_manual_baseline_supersession: bool = False
    owner_confirmed_manual_placeholder_resolutions: list[OwnerConfirmedManualPlaceholderResolution] = Field(
        default_factory=list, max_length=10
    )


class IngestionConfirmResponse(StrictModel):
    batch_id: str
    audit_id: str
    idempotent: bool
    written_records: int | None = None
    payload_hash: str | None = None


class IngestionHistoryItem(StrictModel):
    batch_id: str
    source_key: str
    scope_kind: str
    scope_id: str | None
    period_from: str
    period_to: str
    data_cutoff: str
    input_fingerprint: str
    status: str
    confirmed_at: str
    counts: dict[str, int]
    counts_json: None = None


class IngestionHistoryResponse(StrictModel):
    items: list[IngestionHistoryItem]
    limit: int
    offset: int
    total: int


class ReconciliationMetric(StrictModel):
    ledger: str | None = None
    reported: str | None = None
    difference: str | None
    status: Literal["matched", "within_tolerance", "mismatch", "not_comparable", "unavailable"]


class ValuationMetric(StrictModel):
    derived: str | None
    reported: str | None
    difference: str | None
    relative_difference: str | None
    status: Literal["matched", "within_tolerance", "mismatch", "not_comparable", "unavailable"]
    position_as_of: str | None
    price_as_of: str | None
    fx_as_of: str | None
    price_source: str | None
    fx_source: str | None


class ReconciliationDifference(StrictModel):
    difference_ref: str
    account_ref: str
    account_label: str
    instrument_ref: str
    instrument_label: str
    quantity: ReconciliationMetric
    valuation: ValuationMetric
    reason_codes: list[str]


class AccountTotal(StrictModel):
    account_ref: str
    account_label: str
    total_value_only: bool
    positions_value: str | None
    cash_value: str | None
    derived_total: str | None
    reported_total: str | None
    difference: str | None
    status: Literal["matched", "within_tolerance", "mismatch", "not_comparable", "unavailable"]
    as_of: str | None
    reason_codes: list[str]


class CoverageMetric(StrictModel):
    covered: int
    total: int
    ratio: str | None


class ToleranceContract(StrictModel):
    version: str
    absolute: str
    relative: str


class ReconciliationResponse(StrictModel):
    as_of: str
    data_cutoff: str
    base_currency: str
    status: Literal["matched", "within_tolerance", "mismatch", "not_comparable", "unavailable"]
    reason_codes: list[str]
    tolerance: ToleranceContract
    coverage: dict[str, CoverageMetric]
    differences: list[ReconciliationDifference]
    account_totals: list[AccountTotal]
    input_fingerprint: str
    sources: list[str] = Field(default_factory=list)
    limit: int
    offset: int
    total: int
